Skip to content

fix(routing): apply each target's client-IP allowlist in group dispatch - #786

Merged
jarvis9443 merged 1 commit into
mainfrom
fix/model-group-member-ip-allowlist
Jul 20, 2026
Merged

fix(routing): apply each target's client-IP allowlist in group dispatch#786
jarvis9443 merged 1 commit into
mainfrom
fix/model-group-member-ip-allowlist

Conversation

@jarvis9443

@jarvis9443 jarvis9443 commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Problem

A model's allowed_cidrs was only checked on the alias the caller named (dispatch::check_ip_access, pre-dispatch). Reaching that model as a Model Group target skipped the check entirely — so a direct request to m-internal correctly returned 403, while the same model behind a group served 200 to any caller. Adding an IP-restricted model to a group silently published it.

Same class as api7/AISIX-Cloud#1087 (member rate limits ignored in group dispatch, fixed in #783), on the other per-model gate.

Fix

Filter the candidate set by the caller's source IP in resolve_attempt_models, before the strategy picks:

  • an out-of-range target is not a candidate — the group serves from whatever remains, and the excluded member is never attempted (not attempted-then-failed-over);
  • a group whose every target excludes the caller returns the same 403 ip_restricted the direct path returns;
  • running before selection means max_fallbacks budgets attempts across the targets this caller can actually reach, and a metric-based strategy ranks only those.

RoutingRequest gains source_ip; it defaults to "", which Model::ip_allowed treats as out-of-range, so a call site that forgets to thread it fails closed rather than silently disabling the allowlist.

Deliberately not folded into filter_attempt_models: that filter's when_all_unavailable: try_anyway policy hands back the unfiltered candidate list, which would send the request to a target the operator just declared off-limits. An allowlist has no "try anyway".

The group's own allowed_cidrs stays enforced pre-dispatch — the two tiers are additive. The rejection keeps the generic #557 envelope (no model name, no CIDR), so a group can't be used to probe which members exist or what ranges they allow.

Behavior change

A group with an IP-restricted member now excludes that member for out-of-range callers, instead of dispatching to it. A group whose members are all restricted returns 403 where it previously returned 200. This is the point of the fix, but it is a live-traffic change for anyone who had (knowingly or not) been relying on the bypass. The api7/docs line stating that allowed_cidrs "applies to the model alias the caller names" is corrected in the paired docs PR.

Tests

Extends model-ip-restriction-e2e.test.ts (both new cases fail pre-fix, pass post-fix):

  • out-of-range caller on a [restricted, open] group is served by the open member — asserted on a marker string from the open member's own upstream, because both members answer 200 and an earlier request-count assertion passed pre-fix too;
  • every-member-restricted group → 403, upstream untouched, and the message leaks neither member name nor CIDR.

Plus five routing:: unit tests for the filter itself: drops only the out-of-range target, empty when all excluded, fails closed on an unattributable IP, leaves unresolvable names for the existing config-error path, and is a no-op when nothing restricts.

Fixes api7/AISIX-Cloud#1089

Summary by CodeRabbit

  • New Features

    • Model groups now respect per-model IP allowlists when selecting eligible targets.
    • Requests from restricted IP addresses automatically use permitted group members when available.
    • Groups with no permitted models return a clear access restriction instead of contacting an upstream service.
  • Bug Fixes

    • Routing decisions now consistently include the caller’s source IP across supported request types.
    • Restricted model details are not exposed in access-denied responses.

A model's `allowed_cidrs` was only checked on the alias the caller named.
Reaching that model as a Model Group target skipped it entirely, so
adding an IP-restricted model to a group silently published it to every
caller — a direct request got 403 while the same model behind a group
served 200.

Filter the candidate set by the caller's source IP in
`resolve_attempt_models`, before the strategy picks a target: an
out-of-range target is not a candidate, the group serves from whatever
remains, and a group whose every target excludes the caller returns the
same 403 the direct path returns. Running before selection means
`max_fallbacks` budgets attempts across reachable targets and a
metric-based strategy ranks only those.

Deliberately not folded into `filter_attempt_models`: that filter's
`when_all_unavailable: try_anyway` policy hands back the unfiltered
candidate list, which would send the request to a target the operator
just declared off-limits. An allowlist has no "try anyway".

The group's own `allowed_cidrs` stays enforced pre-dispatch — the two
tiers are additive — and the rejection keeps the generic #557 envelope,
so a group can't be used to probe which members exist or what ranges
they allow.

Behavior change: a group with an IP-restricted member now excludes that
member for out-of-range callers instead of dispatching to it.

Fixes api7/AISIX-Cloud#1089
@coderabbitai

coderabbitai Bot commented Jul 20, 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: 0a63dfbd-101f-4bcd-b09b-be875f7a7f92

📥 Commits

Reviewing files that changed from the base of the PR and between 37c3491 and 242b1b6.

📒 Files selected for processing (7)
  • crates/aisix-proxy/AGENTS.md
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/responses.rs
  • crates/aisix-proxy/src/routing.rs
  • tests/e2e/src/cases/model-ip-restriction-e2e.test.ts

📝 Walkthrough

Walkthrough

Model-group routing now receives the client source IP, filters members against their allowed_cidrs before strategy selection, preserves configuration errors for unresolved targets, and adds unit and end-to-end coverage for mixed and fully restricted groups.

Changes

Model group IP allowlist enforcement

Layer / File(s) Summary
Per-target IP filtering
crates/aisix-proxy/src/routing.rs
RoutingRequest carries source_ip; group targets are filtered by member allowed_cidrs before strategy selection, with ModelIpRestricted returned when no candidates remain.
Source IP propagation
crates/aisix-proxy/src/chat.rs, crates/aisix-proxy/src/count_tokens.rs, crates/aisix-proxy/src/messages.rs, crates/aisix-proxy/src/responses.rs
Dispatch paths pass the client source IP into routing resolution.
Group scenarios and enforcement guidance
crates/aisix-proxy/AGENTS.md, crates/aisix-proxy/src/routing.rs, tests/e2e/src/cases/model-ip-restriction-e2e.test.ts
Documentation, unit tests, and end-to-end tests cover mixed groups, fully restricted groups, unresolved targets, and empty source IPs.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant messages_dispatch
  participant resolve_attempt_models
  participant targets_allowed_for_ip
  participant UpstreamModel
  Client->>messages_dispatch: request with source_ip
  messages_dispatch->>resolve_attempt_models: RoutingRequest
  resolve_attempt_models->>targets_allowed_for_ip: group targets and source_ip
  targets_allowed_for_ip-->>resolve_attempt_models: allowed targets
  resolve_attempt_models->>UpstreamModel: selected target request
Loading

Suggested reviewers: moonming

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change to group routing and client-IP allowlist enforcement.
Linked Issues check ✅ Passed The PR implements the required member-level allowed_cidrs filtering for model groups, preserves remaining targets, returns 403 when all are excluded, and updates docs.
Out of Scope Changes check ✅ Passed The changes stay focused on routing, documentation, and tests needed for the IP-allowlist fix.
E2e Test Quality Review ✅ Passed PASS: The PR adds a real E2E flow through spawned app+etcd+upstreams, covers mixed/all-restricted group cases plus boundary/helper tests, and the assertions are clear and order-independent.
Security Check ✅ Passed No secret/PII leakage, auth bypass, ownership, TLS, shared-resource, or secret-resolution issues found in the changed routing/dispatch code.
✨ 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/model-group-member-ip-allowlist

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

@jarvis9443
jarvis9443 merged commit 9e64a67 into main Jul 20, 2026
10 checks passed
@jarvis9443
jarvis9443 deleted the fix/model-group-member-ip-allowlist branch July 20, 2026 09:18
Sign up for free to 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.

1 participant