fix(passthrough): enforce the borrowed model's client-IP allowlist (#557 parity) - #704

Merged
jarvis9443 merged 1 commit into
mainfrom
fix/issue-697-passthrough-ip
Jul 2, 2026
Merged

fix(passthrough): enforce the borrowed model's client-IP allowlist (#557 parity)#704
jarvis9443 merged 1 commit into
mainfrom
fix/issue-697-passthrough-ip

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Problem

Audit finding #697 (parent: api7/AISIX-Cloud#950). Every typed handler gates on dispatch::check_ip_access (#557), but the raw /passthrough/:provider/* tunnel resolved a model entry — which can carry allowed_cidrs — without checking it, and never resolved a client IP at all. A per-model IP restriction was bypassable by lending the same credentials through passthrough.

Fix

Extract ClientContext in the passthrough handler and call check_ip_access on the borrowed model right after the #449 ACL resolution — the same borrowed-model basis the #911 [6] guardrail resolution uses. Requests with no resolvable peer fail closed against a configured allowlist, matching the typed handlers.

LiteLLM has no per-model client-IP allowlist equivalent on its passthrough routes — no baseline to compare against; this restores internal parity with our own #557 feature.

Tests

Router-level: a model with allowed_cidrs: [10.0.0.0/8] → oneshot (no peer) gets 403 permission_denied and the upstream is never contacted; injecting ConnectInfo(10.1.2.3) passes through 200. Verified fail-before/pass-after.

Fixes#697

Summary by CodeRabbit

  • Bug Fixes
    • Requests now respect IP-based access rules for passthrough traffic.
    • If a client IP is not allowed, the request is rejected with 403 Forbidden before reaching the upstream service.
    • Requests from permitted IPs continue to work normally through the passthrough flow.

 parity)
Every typed handler gates on dispatch::check_ip_access, but the raw
/passthrough/:provider/* tunnel resolved a model entry (which can carry
allowed_cidrs) without ever checking it — and never resolved a client
IP at all. An operator's per-model IP restriction was bypassable by
lending the same credentials through passthrough.
Extract ClientContext in the handler and gate on the borrowed model's
allowed_cidrs right after ACL resolution — the same borrowed-model
basis as the #911 [6] guardrail chain. Oneshot/no-peer requests fail
closed against a configured allowlist, matching the typed handlers.
Fixes#697
@coderabbitai

coderabbitaiBot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3212a58d-3c68-43d2-b24d-f5df89cf4cf1

📥 Commits

Reviewing files that changed from the base of the PR and between cb1bcd0 and a0637c4.

📒 Files selected for processing (1)
  • crates/aisix-proxy/src/passthrough.rs

📝 Walkthrough

Walkthrough

The passthrough handler now captures client IP context via ClientContext and threads source_ip into the internal dispatcher. The dispatcher enforces the resolved model's allowed_cidrs by calling check_ip_access before resolving provider credentials, returning 403 for disallowed IPs. Tests cover both outcomes.

Changes

Passthrough IP Allowlist Enforcement

Layer / File(s)Summary
Thread client IP through handler and dispatcher
crates/aisix-proxy/src/passthrough.rs
The passthrough handler signature gains a ClientContext parameter, passes client.source_ip to dispatch, and the internal dispatch function signature is extended with a source_ip: &str parameter.
Enforce allowlist check and add coverage
crates/aisix-proxy/src/passthrough.rs
dispatch calls check_ip_access(model, source_ip) after model resolution and before contacting upstream; new tests verify a 403 for disallowed client IPs and success for allowed IPs injected via ConnectInfo.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
participant Client
participant PassthroughHandler
participant Dispatch
participant Upstream
Client->>PassthroughHandler: request with ConnectInfo
PassthroughHandler->>PassthroughHandler: extract ClientContext.source_ip
PassthroughHandler->>Dispatch: dispatch(..., source_ip)
Dispatch->>Dispatch: check_ip_access(model, source_ip)
alt IP not allowed
Dispatch-->>PassthroughHandler: 403 Forbidden
else IP allowed
Dispatch->>Upstream: forward request
Upstream-->>Dispatch: response
Dispatch-->>PassthroughHandler: response
end
Loading

Possibly related PRs

  • api7/aisix#516: Both PRs add and thread per-request client IP context via ClientContext/ConnectInfo, with this PR's passthrough handler using that source_ip for access gating.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningThe new checks are in-process router tests (Router::oneshot) with a WireMock upstream, so they don’t reach a true E2E boundary.Add at least one listener-backed end-to-end test (or clearly scope these as integration tests) that drives the running proxy and validates the full request path.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: enforcing passthrough client-IP allowlists.
Linked Issues check✅ PassedThe PR matches #697 by extracting client IP context and checking the model allowlist before passthrough dispatch.
Out of Scope Changes check✅ PassedThe changes stay focused on passthrough IP-allowlist enforcement and its tests, with no unrelated scope detected.
Security Check✅ PassedNo security issues: passthrough now enforces check_ip_access before dispatch, and tests confirm 403/no upstream for disallowed IPs and 200 for allowed IPs.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-697-passthrough-ip

Comment @coderabbitai help to get the list of available commands.

@jarvis9443
jarvis9443 merged commit 99bba33 into mainJul 2, 2026
10 checks passed
@jarvis9443
jarvis9443 deleted the fix/issue-697-passthrough-ip branch July 2, 2026 13:44
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

passthrough endpoints skip the per-model client-IP allowlist (#557 check_ip_access)

1 participant

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(passthrough): enforce the borrowed model's client-IP allowlist (#557 parity) - #704

Merged
jarvis9443 merged 1 commit into
mainfrom
fix/issue-697-passthrough-ip
Jul 2, 2026
Merged

fix(passthrough): enforce the borrowed model's client-IP allowlist (#557 parity)#704
jarvis9443 merged 1 commit into
mainfrom
fix/issue-697-passthrough-ip

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Problem

Audit finding #697 (parent: api7/AISIX-Cloud#950). Every typed handler gates on dispatch::check_ip_access (#557), but the raw /passthrough/:provider/* tunnel resolved a model entry — which can carry allowed_cidrs — without checking it, and never resolved a client IP at all. A per-model IP restriction was bypassable by lending the same credentials through passthrough.

Fix

Extract ClientContext in the passthrough handler and call check_ip_access on the borrowed model right after the #449 ACL resolution — the same borrowed-model basis the #911 [6] guardrail resolution uses. Requests with no resolvable peer fail closed against a configured allowlist, matching the typed handlers.

LiteLLM has no per-model client-IP allowlist equivalent on its passthrough routes — no baseline to compare against; this restores internal parity with our own #557 feature.

Tests

Router-level: a model with allowed_cidrs: [10.0.0.0/8] → oneshot (no peer) gets 403 permission_denied and the upstream is never contacted; injecting ConnectInfo(10.1.2.3) passes through 200. Verified fail-before/pass-after.

Fixes#697

Summary by CodeRabbit

  • Bug Fixes
    • Requests now respect IP-based access rules for passthrough traffic.
    • If a client IP is not allowed, the request is rejected with 403 Forbidden before reaching the upstream service.
    • Requests from permitted IPs continue to work normally through the passthrough flow.

 parity)
Every typed handler gates on dispatch::check_ip_access, but the raw
/passthrough/:provider/* tunnel resolved a model entry (which can carry
allowed_cidrs) without ever checking it — and never resolved a client
IP at all. An operator's per-model IP restriction was bypassable by
lending the same credentials through passthrough.
Extract ClientContext in the handler and gate on the borrowed model's
allowed_cidrs right after ACL resolution — the same borrowed-model
basis as the #911 [6] guardrail chain. Oneshot/no-peer requests fail
closed against a configured allowlist, matching the typed handlers.
Fixes#697
@coderabbitai

coderabbitaiBot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3212a58d-3c68-43d2-b24d-f5df89cf4cf1

📥 Commits

Reviewing files that changed from the base of the PR and between cb1bcd0 and a0637c4.

📒 Files selected for processing (1)
  • crates/aisix-proxy/src/passthrough.rs

📝 Walkthrough

Walkthrough

The passthrough handler now captures client IP context via ClientContext and threads source_ip into the internal dispatcher. The dispatcher enforces the resolved model's allowed_cidrs by calling check_ip_access before resolving provider credentials, returning 403 for disallowed IPs. Tests cover both outcomes.

Changes

Passthrough IP Allowlist Enforcement

Layer / File(s)Summary
Thread client IP through handler and dispatcher
crates/aisix-proxy/src/passthrough.rs
The passthrough handler signature gains a ClientContext parameter, passes client.source_ip to dispatch, and the internal dispatch function signature is extended with a source_ip: &str parameter.
Enforce allowlist check and add coverage
crates/aisix-proxy/src/passthrough.rs
dispatch calls check_ip_access(model, source_ip) after model resolution and before contacting upstream; new tests verify a 403 for disallowed client IPs and success for allowed IPs injected via ConnectInfo.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
participant Client
participant PassthroughHandler
participant Dispatch
participant Upstream
Client->>PassthroughHandler: request with ConnectInfo
PassthroughHandler->>PassthroughHandler: extract ClientContext.source_ip
PassthroughHandler->>Dispatch: dispatch(..., source_ip)
Dispatch->>Dispatch: check_ip_access(model, source_ip)
alt IP not allowed
Dispatch-->>PassthroughHandler: 403 Forbidden
else IP allowed
Dispatch->>Upstream: forward request
Upstream-->>Dispatch: response
Dispatch-->>PassthroughHandler: response
end
Loading

Possibly related PRs

  • api7/aisix#516: Both PRs add and thread per-request client IP context via ClientContext/ConnectInfo, with this PR's passthrough handler using that source_ip for access gating.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningThe new checks are in-process router tests (Router::oneshot) with a WireMock upstream, so they don’t reach a true E2E boundary.Add at least one listener-backed end-to-end test (or clearly scope these as integration tests) that drives the running proxy and validates the full request path.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: enforcing passthrough client-IP allowlists.
Linked Issues check✅ PassedThe PR matches #697 by extracting client IP context and checking the model allowlist before passthrough dispatch.
Out of Scope Changes check✅ PassedThe changes stay focused on passthrough IP-allowlist enforcement and its tests, with no unrelated scope detected.
Security Check✅ PassedNo security issues: passthrough now enforces check_ip_access before dispatch, and tests confirm 403/no upstream for disallowed IPs and 200 for allowed IPs.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-697-passthrough-ip

Comment @coderabbitai help to get the list of available commands.

@jarvis9443
jarvis9443 merged commit 99bba33 into mainJul 2, 2026
10 checks passed
@jarvis9443
jarvis9443 deleted the fix/issue-697-passthrough-ip branch July 2, 2026 13:44
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

passthrough endpoints skip the per-model client-IP allowlist (#557 check_ip_access)

1 participant

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(passthrough): enforce the borrowed model's client-IP allowlist (#557 parity) - #704

Merged
jarvis9443 merged 1 commit into
mainfrom
fix/issue-697-passthrough-ip
Jul 2, 2026
Merged

fix(passthrough): enforce the borrowed model's client-IP allowlist (#557 parity)#704
jarvis9443 merged 1 commit into
mainfrom
fix/issue-697-passthrough-ip

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Problem

Audit finding #697 (parent: api7/AISIX-Cloud#950). Every typed handler gates on dispatch::check_ip_access (#557), but the raw /passthrough/:provider/* tunnel resolved a model entry — which can carry allowed_cidrs — without checking it, and never resolved a client IP at all. A per-model IP restriction was bypassable by lending the same credentials through passthrough.

Fix

Extract ClientContext in the passthrough handler and call check_ip_access on the borrowed model right after the #449 ACL resolution — the same borrowed-model basis the #911 [6] guardrail resolution uses. Requests with no resolvable peer fail closed against a configured allowlist, matching the typed handlers.

LiteLLM has no per-model client-IP allowlist equivalent on its passthrough routes — no baseline to compare against; this restores internal parity with our own #557 feature.

Tests

Router-level: a model with allowed_cidrs: [10.0.0.0/8] → oneshot (no peer) gets 403 permission_denied and the upstream is never contacted; injecting ConnectInfo(10.1.2.3) passes through 200. Verified fail-before/pass-after.

Fixes#697

Summary by CodeRabbit

  • Bug Fixes
    • Requests now respect IP-based access rules for passthrough traffic.
    • If a client IP is not allowed, the request is rejected with 403 Forbidden before reaching the upstream service.
    • Requests from permitted IPs continue to work normally through the passthrough flow.

 parity)
Every typed handler gates on dispatch::check_ip_access, but the raw
/passthrough/:provider/* tunnel resolved a model entry (which can carry
allowed_cidrs) without ever checking it — and never resolved a client
IP at all. An operator's per-model IP restriction was bypassable by
lending the same credentials through passthrough.
Extract ClientContext in the handler and gate on the borrowed model's
allowed_cidrs right after ACL resolution — the same borrowed-model
basis as the #911 [6] guardrail chain. Oneshot/no-peer requests fail
closed against a configured allowlist, matching the typed handlers.
Fixes#697
@coderabbitai

coderabbitaiBot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3212a58d-3c68-43d2-b24d-f5df89cf4cf1

📥 Commits

Reviewing files that changed from the base of the PR and between cb1bcd0 and a0637c4.

📒 Files selected for processing (1)
  • crates/aisix-proxy/src/passthrough.rs

📝 Walkthrough

Walkthrough

The passthrough handler now captures client IP context via ClientContext and threads source_ip into the internal dispatcher. The dispatcher enforces the resolved model's allowed_cidrs by calling check_ip_access before resolving provider credentials, returning 403 for disallowed IPs. Tests cover both outcomes.

Changes

Passthrough IP Allowlist Enforcement

Layer / File(s)Summary
Thread client IP through handler and dispatcher
crates/aisix-proxy/src/passthrough.rs
The passthrough handler signature gains a ClientContext parameter, passes client.source_ip to dispatch, and the internal dispatch function signature is extended with a source_ip: &str parameter.
Enforce allowlist check and add coverage
crates/aisix-proxy/src/passthrough.rs
dispatch calls check_ip_access(model, source_ip) after model resolution and before contacting upstream; new tests verify a 403 for disallowed client IPs and success for allowed IPs injected via ConnectInfo.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
participant Client
participant PassthroughHandler
participant Dispatch
participant Upstream
Client->>PassthroughHandler: request with ConnectInfo
PassthroughHandler->>PassthroughHandler: extract ClientContext.source_ip
PassthroughHandler->>Dispatch: dispatch(..., source_ip)
Dispatch->>Dispatch: check_ip_access(model, source_ip)
alt IP not allowed
Dispatch-->>PassthroughHandler: 403 Forbidden
else IP allowed
Dispatch->>Upstream: forward request
Upstream-->>Dispatch: response
Dispatch-->>PassthroughHandler: response
end
Loading

Possibly related PRs

  • api7/aisix#516: Both PRs add and thread per-request client IP context via ClientContext/ConnectInfo, with this PR's passthrough handler using that source_ip for access gating.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningThe new checks are in-process router tests (Router::oneshot) with a WireMock upstream, so they don’t reach a true E2E boundary.Add at least one listener-backed end-to-end test (or clearly scope these as integration tests) that drives the running proxy and validates the full request path.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: enforcing passthrough client-IP allowlists.
Linked Issues check✅ PassedThe PR matches #697 by extracting client IP context and checking the model allowlist before passthrough dispatch.
Out of Scope Changes check✅ PassedThe changes stay focused on passthrough IP-allowlist enforcement and its tests, with no unrelated scope detected.
Security Check✅ PassedNo security issues: passthrough now enforces check_ip_access before dispatch, and tests confirm 403/no upstream for disallowed IPs and 200 for allowed IPs.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-697-passthrough-ip

Comment @coderabbitai help to get the list of available commands.

@jarvis9443
jarvis9443 merged commit 99bba33 into mainJul 2, 2026
10 checks passed
@jarvis9443
jarvis9443 deleted the fix/issue-697-passthrough-ip branch July 2, 2026 13:44
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

passthrough endpoints skip the per-model client-IP allowlist (#557 check_ip_access)

1 participant

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(passthrough): enforce the borrowed model's client-IP allowlist (#557 parity) - #704

Merged
jarvis9443 merged 1 commit into
mainfrom
fix/issue-697-passthrough-ip
Jul 2, 2026
Merged

fix(passthrough): enforce the borrowed model's client-IP allowlist (#557 parity)#704
jarvis9443 merged 1 commit into
mainfrom
fix/issue-697-passthrough-ip

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Problem

Audit finding #697 (parent: api7/AISIX-Cloud#950). Every typed handler gates on dispatch::check_ip_access (#557), but the raw /passthrough/:provider/* tunnel resolved a model entry — which can carry allowed_cidrs — without checking it, and never resolved a client IP at all. A per-model IP restriction was bypassable by lending the same credentials through passthrough.

Fix

Extract ClientContext in the passthrough handler and call check_ip_access on the borrowed model right after the #449 ACL resolution — the same borrowed-model basis the #911 [6] guardrail resolution uses. Requests with no resolvable peer fail closed against a configured allowlist, matching the typed handlers.

LiteLLM has no per-model client-IP allowlist equivalent on its passthrough routes — no baseline to compare against; this restores internal parity with our own #557 feature.

Tests

Router-level: a model with allowed_cidrs: [10.0.0.0/8] → oneshot (no peer) gets 403 permission_denied and the upstream is never contacted; injecting ConnectInfo(10.1.2.3) passes through 200. Verified fail-before/pass-after.

Fixes#697

Summary by CodeRabbit

  • Bug Fixes
    • Requests now respect IP-based access rules for passthrough traffic.
    • If a client IP is not allowed, the request is rejected with 403 Forbidden before reaching the upstream service.
    • Requests from permitted IPs continue to work normally through the passthrough flow.

 parity)
Every typed handler gates on dispatch::check_ip_access, but the raw
/passthrough/:provider/* tunnel resolved a model entry (which can carry
allowed_cidrs) without ever checking it — and never resolved a client
IP at all. An operator's per-model IP restriction was bypassable by
lending the same credentials through passthrough.
Extract ClientContext in the handler and gate on the borrowed model's
allowed_cidrs right after ACL resolution — the same borrowed-model
basis as the #911 [6] guardrail chain. Oneshot/no-peer requests fail
closed against a configured allowlist, matching the typed handlers.
Fixes#697
@coderabbitai

coderabbitaiBot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3212a58d-3c68-43d2-b24d-f5df89cf4cf1

📥 Commits

Reviewing files that changed from the base of the PR and between cb1bcd0 and a0637c4.

📒 Files selected for processing (1)
  • crates/aisix-proxy/src/passthrough.rs

📝 Walkthrough

Walkthrough

The passthrough handler now captures client IP context via ClientContext and threads source_ip into the internal dispatcher. The dispatcher enforces the resolved model's allowed_cidrs by calling check_ip_access before resolving provider credentials, returning 403 for disallowed IPs. Tests cover both outcomes.

Changes

Passthrough IP Allowlist Enforcement

Layer / File(s)Summary
Thread client IP through handler and dispatcher
crates/aisix-proxy/src/passthrough.rs
The passthrough handler signature gains a ClientContext parameter, passes client.source_ip to dispatch, and the internal dispatch function signature is extended with a source_ip: &str parameter.
Enforce allowlist check and add coverage
crates/aisix-proxy/src/passthrough.rs
dispatch calls check_ip_access(model, source_ip) after model resolution and before contacting upstream; new tests verify a 403 for disallowed client IPs and success for allowed IPs injected via ConnectInfo.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
participant Client
participant PassthroughHandler
participant Dispatch
participant Upstream
Client->>PassthroughHandler: request with ConnectInfo
PassthroughHandler->>PassthroughHandler: extract ClientContext.source_ip
PassthroughHandler->>Dispatch: dispatch(..., source_ip)
Dispatch->>Dispatch: check_ip_access(model, source_ip)
alt IP not allowed
Dispatch-->>PassthroughHandler: 403 Forbidden
else IP allowed
Dispatch->>Upstream: forward request
Upstream-->>Dispatch: response
Dispatch-->>PassthroughHandler: response
end
Loading

Possibly related PRs

  • api7/aisix#516: Both PRs add and thread per-request client IP context via ClientContext/ConnectInfo, with this PR's passthrough handler using that source_ip for access gating.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningThe new checks are in-process router tests (Router::oneshot) with a WireMock upstream, so they don’t reach a true E2E boundary.Add at least one listener-backed end-to-end test (or clearly scope these as integration tests) that drives the running proxy and validates the full request path.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: enforcing passthrough client-IP allowlists.
Linked Issues check✅ PassedThe PR matches #697 by extracting client IP context and checking the model allowlist before passthrough dispatch.
Out of Scope Changes check✅ PassedThe changes stay focused on passthrough IP-allowlist enforcement and its tests, with no unrelated scope detected.
Security Check✅ PassedNo security issues: passthrough now enforces check_ip_access before dispatch, and tests confirm 403/no upstream for disallowed IPs and 200 for allowed IPs.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-697-passthrough-ip

Comment @coderabbitai help to get the list of available commands.

@jarvis9443
jarvis9443 merged commit 99bba33 into mainJul 2, 2026
10 checks passed
@jarvis9443
jarvis9443 deleted the fix/issue-697-passthrough-ip branch July 2, 2026 13:44
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

passthrough endpoints skip the per-model client-IP allowlist (#557 check_ip_access)

1 participant

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(passthrough): enforce the borrowed model's client-IP allowlist (#557 parity) - #704

Merged
jarvis9443 merged 1 commit into
mainfrom
fix/issue-697-passthrough-ip
Jul 2, 2026
Merged

fix(passthrough): enforce the borrowed model's client-IP allowlist (#557 parity)#704
jarvis9443 merged 1 commit into
mainfrom
fix/issue-697-passthrough-ip

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Problem

Audit finding #697 (parent: api7/AISIX-Cloud#950). Every typed handler gates on dispatch::check_ip_access (#557), but the raw /passthrough/:provider/* tunnel resolved a model entry — which can carry allowed_cidrs — without checking it, and never resolved a client IP at all. A per-model IP restriction was bypassable by lending the same credentials through passthrough.

Fix

Extract ClientContext in the passthrough handler and call check_ip_access on the borrowed model right after the #449 ACL resolution — the same borrowed-model basis the #911 [6] guardrail resolution uses. Requests with no resolvable peer fail closed against a configured allowlist, matching the typed handlers.

LiteLLM has no per-model client-IP allowlist equivalent on its passthrough routes — no baseline to compare against; this restores internal parity with our own #557 feature.

Tests

Router-level: a model with allowed_cidrs: [10.0.0.0/8] → oneshot (no peer) gets 403 permission_denied and the upstream is never contacted; injecting ConnectInfo(10.1.2.3) passes through 200. Verified fail-before/pass-after.

Fixes#697

Summary by CodeRabbit

  • Bug Fixes
    • Requests now respect IP-based access rules for passthrough traffic.
    • If a client IP is not allowed, the request is rejected with 403 Forbidden before reaching the upstream service.
    • Requests from permitted IPs continue to work normally through the passthrough flow.

 parity)
Every typed handler gates on dispatch::check_ip_access, but the raw
/passthrough/:provider/* tunnel resolved a model entry (which can carry
allowed_cidrs) without ever checking it — and never resolved a client
IP at all. An operator's per-model IP restriction was bypassable by
lending the same credentials through passthrough.
Extract ClientContext in the handler and gate on the borrowed model's
allowed_cidrs right after ACL resolution — the same borrowed-model
basis as the #911 [6] guardrail chain. Oneshot/no-peer requests fail
closed against a configured allowlist, matching the typed handlers.
Fixes#697
@coderabbitai

coderabbitaiBot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3212a58d-3c68-43d2-b24d-f5df89cf4cf1

📥 Commits

Reviewing files that changed from the base of the PR and between cb1bcd0 and a0637c4.

📒 Files selected for processing (1)
  • crates/aisix-proxy/src/passthrough.rs

📝 Walkthrough

Walkthrough

The passthrough handler now captures client IP context via ClientContext and threads source_ip into the internal dispatcher. The dispatcher enforces the resolved model's allowed_cidrs by calling check_ip_access before resolving provider credentials, returning 403 for disallowed IPs. Tests cover both outcomes.

Changes

Passthrough IP Allowlist Enforcement

Layer / File(s)Summary
Thread client IP through handler and dispatcher
crates/aisix-proxy/src/passthrough.rs
The passthrough handler signature gains a ClientContext parameter, passes client.source_ip to dispatch, and the internal dispatch function signature is extended with a source_ip: &str parameter.
Enforce allowlist check and add coverage
crates/aisix-proxy/src/passthrough.rs
dispatch calls check_ip_access(model, source_ip) after model resolution and before contacting upstream; new tests verify a 403 for disallowed client IPs and success for allowed IPs injected via ConnectInfo.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
participant Client
participant PassthroughHandler
participant Dispatch
participant Upstream
Client->>PassthroughHandler: request with ConnectInfo
PassthroughHandler->>PassthroughHandler: extract ClientContext.source_ip
PassthroughHandler->>Dispatch: dispatch(..., source_ip)
Dispatch->>Dispatch: check_ip_access(model, source_ip)
alt IP not allowed
Dispatch-->>PassthroughHandler: 403 Forbidden
else IP allowed
Dispatch->>Upstream: forward request
Upstream-->>Dispatch: response
Dispatch-->>PassthroughHandler: response
end
Loading

Possibly related PRs

  • api7/aisix#516: Both PRs add and thread per-request client IP context via ClientContext/ConnectInfo, with this PR's passthrough handler using that source_ip for access gating.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningThe new checks are in-process router tests (Router::oneshot) with a WireMock upstream, so they don’t reach a true E2E boundary.Add at least one listener-backed end-to-end test (or clearly scope these as integration tests) that drives the running proxy and validates the full request path.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: enforcing passthrough client-IP allowlists.
Linked Issues check✅ PassedThe PR matches #697 by extracting client IP context and checking the model allowlist before passthrough dispatch.
Out of Scope Changes check✅ PassedThe changes stay focused on passthrough IP-allowlist enforcement and its tests, with no unrelated scope detected.
Security Check✅ PassedNo security issues: passthrough now enforces check_ip_access before dispatch, and tests confirm 403/no upstream for disallowed IPs and 200 for allowed IPs.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-697-passthrough-ip

Comment @coderabbitai help to get the list of available commands.

@jarvis9443
jarvis9443 merged commit 99bba33 into mainJul 2, 2026
10 checks passed
@jarvis9443
jarvis9443 deleted the fix/issue-697-passthrough-ip branch July 2, 2026 13:44
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

passthrough endpoints skip the per-model client-IP allowlist (#557 check_ip_access)

1 participant

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(passthrough): enforce the borrowed model's client-IP allowlist (#557 parity) - #704

Merged
jarvis9443 merged 1 commit into
mainfrom
fix/issue-697-passthrough-ip
Jul 2, 2026
Merged

fix(passthrough): enforce the borrowed model's client-IP allowlist (#557 parity)#704
jarvis9443 merged 1 commit into
mainfrom
fix/issue-697-passthrough-ip

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Problem

Audit finding #697 (parent: api7/AISIX-Cloud#950). Every typed handler gates on dispatch::check_ip_access (#557), but the raw /passthrough/:provider/* tunnel resolved a model entry — which can carry allowed_cidrs — without checking it, and never resolved a client IP at all. A per-model IP restriction was bypassable by lending the same credentials through passthrough.

Fix

Extract ClientContext in the passthrough handler and call check_ip_access on the borrowed model right after the #449 ACL resolution — the same borrowed-model basis the #911 [6] guardrail resolution uses. Requests with no resolvable peer fail closed against a configured allowlist, matching the typed handlers.

LiteLLM has no per-model client-IP allowlist equivalent on its passthrough routes — no baseline to compare against; this restores internal parity with our own #557 feature.

Tests

Router-level: a model with allowed_cidrs: [10.0.0.0/8] → oneshot (no peer) gets 403 permission_denied and the upstream is never contacted; injecting ConnectInfo(10.1.2.3) passes through 200. Verified fail-before/pass-after.

Fixes#697

Summary by CodeRabbit

  • Bug Fixes
    • Requests now respect IP-based access rules for passthrough traffic.
    • If a client IP is not allowed, the request is rejected with 403 Forbidden before reaching the upstream service.
    • Requests from permitted IPs continue to work normally through the passthrough flow.

 parity)
Every typed handler gates on dispatch::check_ip_access, but the raw
/passthrough/:provider/* tunnel resolved a model entry (which can carry
allowed_cidrs) without ever checking it — and never resolved a client
IP at all. An operator's per-model IP restriction was bypassable by
lending the same credentials through passthrough.
Extract ClientContext in the handler and gate on the borrowed model's
allowed_cidrs right after ACL resolution — the same borrowed-model
basis as the #911 [6] guardrail chain. Oneshot/no-peer requests fail
closed against a configured allowlist, matching the typed handlers.
Fixes#697
@coderabbitai

coderabbitaiBot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3212a58d-3c68-43d2-b24d-f5df89cf4cf1

📥 Commits

Reviewing files that changed from the base of the PR and between cb1bcd0 and a0637c4.

📒 Files selected for processing (1)
  • crates/aisix-proxy/src/passthrough.rs

📝 Walkthrough

Walkthrough

The passthrough handler now captures client IP context via ClientContext and threads source_ip into the internal dispatcher. The dispatcher enforces the resolved model's allowed_cidrs by calling check_ip_access before resolving provider credentials, returning 403 for disallowed IPs. Tests cover both outcomes.

Changes

Passthrough IP Allowlist Enforcement

Layer / File(s)Summary
Thread client IP through handler and dispatcher
crates/aisix-proxy/src/passthrough.rs
The passthrough handler signature gains a ClientContext parameter, passes client.source_ip to dispatch, and the internal dispatch function signature is extended with a source_ip: &str parameter.
Enforce allowlist check and add coverage
crates/aisix-proxy/src/passthrough.rs
dispatch calls check_ip_access(model, source_ip) after model resolution and before contacting upstream; new tests verify a 403 for disallowed client IPs and success for allowed IPs injected via ConnectInfo.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
participant Client
participant PassthroughHandler
participant Dispatch
participant Upstream
Client->>PassthroughHandler: request with ConnectInfo
PassthroughHandler->>PassthroughHandler: extract ClientContext.source_ip
PassthroughHandler->>Dispatch: dispatch(..., source_ip)
Dispatch->>Dispatch: check_ip_access(model, source_ip)
alt IP not allowed
Dispatch-->>PassthroughHandler: 403 Forbidden
else IP allowed
Dispatch->>Upstream: forward request
Upstream-->>Dispatch: response
Dispatch-->>PassthroughHandler: response
end
Loading

Possibly related PRs

  • api7/aisix#516: Both PRs add and thread per-request client IP context via ClientContext/ConnectInfo, with this PR's passthrough handler using that source_ip for access gating.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningThe new checks are in-process router tests (Router::oneshot) with a WireMock upstream, so they don’t reach a true E2E boundary.Add at least one listener-backed end-to-end test (or clearly scope these as integration tests) that drives the running proxy and validates the full request path.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: enforcing passthrough client-IP allowlists.
Linked Issues check✅ PassedThe PR matches #697 by extracting client IP context and checking the model allowlist before passthrough dispatch.
Out of Scope Changes check✅ PassedThe changes stay focused on passthrough IP-allowlist enforcement and its tests, with no unrelated scope detected.
Security Check✅ PassedNo security issues: passthrough now enforces check_ip_access before dispatch, and tests confirm 403/no upstream for disallowed IPs and 200 for allowed IPs.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-697-passthrough-ip

Comment @coderabbitai help to get the list of available commands.

@jarvis9443
jarvis9443 merged commit 99bba33 into mainJul 2, 2026
10 checks passed
@jarvis9443
jarvis9443 deleted the fix/issue-697-passthrough-ip branch July 2, 2026 13:44
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

passthrough endpoints skip the per-model client-IP allowlist (#557 check_ip_access)

1 participant

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(passthrough): enforce the borrowed model's client-IP allowlist (#557 parity) - #704

Merged
jarvis9443 merged 1 commit into
mainfrom
fix/issue-697-passthrough-ip
Jul 2, 2026
Merged

fix(passthrough): enforce the borrowed model's client-IP allowlist (#557 parity)#704
jarvis9443 merged 1 commit into
mainfrom
fix/issue-697-passthrough-ip

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Problem

Audit finding #697 (parent: api7/AISIX-Cloud#950). Every typed handler gates on dispatch::check_ip_access (#557), but the raw /passthrough/:provider/* tunnel resolved a model entry — which can carry allowed_cidrs — without checking it, and never resolved a client IP at all. A per-model IP restriction was bypassable by lending the same credentials through passthrough.

Fix

Extract ClientContext in the passthrough handler and call check_ip_access on the borrowed model right after the #449 ACL resolution — the same borrowed-model basis the #911 [6] guardrail resolution uses. Requests with no resolvable peer fail closed against a configured allowlist, matching the typed handlers.

LiteLLM has no per-model client-IP allowlist equivalent on its passthrough routes — no baseline to compare against; this restores internal parity with our own #557 feature.

Tests

Router-level: a model with allowed_cidrs: [10.0.0.0/8] → oneshot (no peer) gets 403 permission_denied and the upstream is never contacted; injecting ConnectInfo(10.1.2.3) passes through 200. Verified fail-before/pass-after.

Fixes#697

Summary by CodeRabbit

  • Bug Fixes
    • Requests now respect IP-based access rules for passthrough traffic.
    • If a client IP is not allowed, the request is rejected with 403 Forbidden before reaching the upstream service.
    • Requests from permitted IPs continue to work normally through the passthrough flow.

 parity)
Every typed handler gates on dispatch::check_ip_access, but the raw
/passthrough/:provider/* tunnel resolved a model entry (which can carry
allowed_cidrs) without ever checking it — and never resolved a client
IP at all. An operator's per-model IP restriction was bypassable by
lending the same credentials through passthrough.
Extract ClientContext in the handler and gate on the borrowed model's
allowed_cidrs right after ACL resolution — the same borrowed-model
basis as the #911 [6] guardrail chain. Oneshot/no-peer requests fail
closed against a configured allowlist, matching the typed handlers.
Fixes#697
@coderabbitai

coderabbitaiBot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3212a58d-3c68-43d2-b24d-f5df89cf4cf1

📥 Commits

Reviewing files that changed from the base of the PR and between cb1bcd0 and a0637c4.

📒 Files selected for processing (1)
  • crates/aisix-proxy/src/passthrough.rs

📝 Walkthrough

Walkthrough

The passthrough handler now captures client IP context via ClientContext and threads source_ip into the internal dispatcher. The dispatcher enforces the resolved model's allowed_cidrs by calling check_ip_access before resolving provider credentials, returning 403 for disallowed IPs. Tests cover both outcomes.

Changes

Passthrough IP Allowlist Enforcement

Layer / File(s)Summary
Thread client IP through handler and dispatcher
crates/aisix-proxy/src/passthrough.rs
The passthrough handler signature gains a ClientContext parameter, passes client.source_ip to dispatch, and the internal dispatch function signature is extended with a source_ip: &str parameter.
Enforce allowlist check and add coverage
crates/aisix-proxy/src/passthrough.rs
dispatch calls check_ip_access(model, source_ip) after model resolution and before contacting upstream; new tests verify a 403 for disallowed client IPs and success for allowed IPs injected via ConnectInfo.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
participant Client
participant PassthroughHandler
participant Dispatch
participant Upstream
Client->>PassthroughHandler: request with ConnectInfo
PassthroughHandler->>PassthroughHandler: extract ClientContext.source_ip
PassthroughHandler->>Dispatch: dispatch(..., source_ip)
Dispatch->>Dispatch: check_ip_access(model, source_ip)
alt IP not allowed
Dispatch-->>PassthroughHandler: 403 Forbidden
else IP allowed
Dispatch->>Upstream: forward request
Upstream-->>Dispatch: response
Dispatch-->>PassthroughHandler: response
end
Loading

Possibly related PRs

  • api7/aisix#516: Both PRs add and thread per-request client IP context via ClientContext/ConnectInfo, with this PR's passthrough handler using that source_ip for access gating.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningThe new checks are in-process router tests (Router::oneshot) with a WireMock upstream, so they don’t reach a true E2E boundary.Add at least one listener-backed end-to-end test (or clearly scope these as integration tests) that drives the running proxy and validates the full request path.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: enforcing passthrough client-IP allowlists.
Linked Issues check✅ PassedThe PR matches #697 by extracting client IP context and checking the model allowlist before passthrough dispatch.
Out of Scope Changes check✅ PassedThe changes stay focused on passthrough IP-allowlist enforcement and its tests, with no unrelated scope detected.
Security Check✅ PassedNo security issues: passthrough now enforces check_ip_access before dispatch, and tests confirm 403/no upstream for disallowed IPs and 200 for allowed IPs.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-697-passthrough-ip

Comment @coderabbitai help to get the list of available commands.

@jarvis9443
jarvis9443 merged commit 99bba33 into mainJul 2, 2026
10 checks passed
@jarvis9443
jarvis9443 deleted the fix/issue-697-passthrough-ip branch July 2, 2026 13:44
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

passthrough endpoints skip the per-model client-IP allowlist (#557 check_ip_access)

1 participant

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(passthrough): enforce the borrowed model's client-IP allowlist (#557 parity) - #704

Merged
jarvis9443 merged 1 commit into
mainfrom
fix/issue-697-passthrough-ip
Jul 2, 2026
Merged

fix(passthrough): enforce the borrowed model's client-IP allowlist (#557 parity)#704
jarvis9443 merged 1 commit into
mainfrom
fix/issue-697-passthrough-ip

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Problem

Audit finding #697 (parent: api7/AISIX-Cloud#950). Every typed handler gates on dispatch::check_ip_access (#557), but the raw /passthrough/:provider/* tunnel resolved a model entry — which can carry allowed_cidrs — without checking it, and never resolved a client IP at all. A per-model IP restriction was bypassable by lending the same credentials through passthrough.

Fix

Extract ClientContext in the passthrough handler and call check_ip_access on the borrowed model right after the #449 ACL resolution — the same borrowed-model basis the #911 [6] guardrail resolution uses. Requests with no resolvable peer fail closed against a configured allowlist, matching the typed handlers.

LiteLLM has no per-model client-IP allowlist equivalent on its passthrough routes — no baseline to compare against; this restores internal parity with our own #557 feature.

Tests

Router-level: a model with allowed_cidrs: [10.0.0.0/8] → oneshot (no peer) gets 403 permission_denied and the upstream is never contacted; injecting ConnectInfo(10.1.2.3) passes through 200. Verified fail-before/pass-after.

Fixes#697

Summary by CodeRabbit

  • Bug Fixes
    • Requests now respect IP-based access rules for passthrough traffic.
    • If a client IP is not allowed, the request is rejected with 403 Forbidden before reaching the upstream service.
    • Requests from permitted IPs continue to work normally through the passthrough flow.

 parity)
Every typed handler gates on dispatch::check_ip_access, but the raw
/passthrough/:provider/* tunnel resolved a model entry (which can carry
allowed_cidrs) without ever checking it — and never resolved a client
IP at all. An operator's per-model IP restriction was bypassable by
lending the same credentials through passthrough.
Extract ClientContext in the handler and gate on the borrowed model's
allowed_cidrs right after ACL resolution — the same borrowed-model
basis as the #911 [6] guardrail chain. Oneshot/no-peer requests fail
closed against a configured allowlist, matching the typed handlers.
Fixes#697
@coderabbitai

coderabbitaiBot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3212a58d-3c68-43d2-b24d-f5df89cf4cf1

📥 Commits

Reviewing files that changed from the base of the PR and between cb1bcd0 and a0637c4.

📒 Files selected for processing (1)
  • crates/aisix-proxy/src/passthrough.rs

📝 Walkthrough

Walkthrough

The passthrough handler now captures client IP context via ClientContext and threads source_ip into the internal dispatcher. The dispatcher enforces the resolved model's allowed_cidrs by calling check_ip_access before resolving provider credentials, returning 403 for disallowed IPs. Tests cover both outcomes.

Changes

Passthrough IP Allowlist Enforcement

Layer / File(s)Summary
Thread client IP through handler and dispatcher
crates/aisix-proxy/src/passthrough.rs
The passthrough handler signature gains a ClientContext parameter, passes client.source_ip to dispatch, and the internal dispatch function signature is extended with a source_ip: &str parameter.
Enforce allowlist check and add coverage
crates/aisix-proxy/src/passthrough.rs
dispatch calls check_ip_access(model, source_ip) after model resolution and before contacting upstream; new tests verify a 403 for disallowed client IPs and success for allowed IPs injected via ConnectInfo.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
participant Client
participant PassthroughHandler
participant Dispatch
participant Upstream
Client->>PassthroughHandler: request with ConnectInfo
PassthroughHandler->>PassthroughHandler: extract ClientContext.source_ip
PassthroughHandler->>Dispatch: dispatch(..., source_ip)
Dispatch->>Dispatch: check_ip_access(model, source_ip)
alt IP not allowed
Dispatch-->>PassthroughHandler: 403 Forbidden
else IP allowed
Dispatch->>Upstream: forward request
Upstream-->>Dispatch: response
Dispatch-->>PassthroughHandler: response
end
Loading

Possibly related PRs

  • api7/aisix#516: Both PRs add and thread per-request client IP context via ClientContext/ConnectInfo, with this PR's passthrough handler using that source_ip for access gating.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningThe new checks are in-process router tests (Router::oneshot) with a WireMock upstream, so they don’t reach a true E2E boundary.Add at least one listener-backed end-to-end test (or clearly scope these as integration tests) that drives the running proxy and validates the full request path.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: enforcing passthrough client-IP allowlists.
Linked Issues check✅ PassedThe PR matches #697 by extracting client IP context and checking the model allowlist before passthrough dispatch.
Out of Scope Changes check✅ PassedThe changes stay focused on passthrough IP-allowlist enforcement and its tests, with no unrelated scope detected.
Security Check✅ PassedNo security issues: passthrough now enforces check_ip_access before dispatch, and tests confirm 403/no upstream for disallowed IPs and 200 for allowed IPs.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-697-passthrough-ip

Comment @coderabbitai help to get the list of available commands.

@jarvis9443
jarvis9443 merged commit 99bba33 into mainJul 2, 2026
10 checks passed
@jarvis9443
jarvis9443 deleted the fix/issue-697-passthrough-ip branch July 2, 2026 13:44
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

passthrough endpoints skip the per-model client-IP allowlist (#557 check_ip_access)

1 participant

@jarvis9443