Bind department networks to tenants¶
Combine authenticated identity with a departmental source network, then try trusted-interface credentials in the isolated lab. Finish each experiment by removing its binding so later scheduling tests use key identity only.
Choose the identity source appropriate for your deployment: CIDR checks the actual peer subnet; ingress_if names a trusted local interface; vlan/vni resolve local software devices. Each payload below shows tenant, services, enforce and credential separately. Change services whenever the inference VIP/port changes; do not include management port 11111.
Before you start¶
Complete identity and retain both departmental keys.
Focus of this page: Add one network binding at a time; VLAN/VXLAN devices exist inside this same software lab.
Topology and execution locations¶
flowchart TD
A["Support namespace<br/>192.0.2.10"] -->|"sa to ga"| G["share-gateway<br/>inference 8080 / API 11111"]
B["Research namespace<br/>198.51.100.10"] -->|"sb to gb"| G
G -->|"ge to se"| E["Receipt backend namespace<br/>203.0.113.1:8080"]
G -.->|"key and objective store"| P["share-pg<br/>Docker bridge / 5432"]
O["Host Bash<br/>private LAB files"] -.->|"management via share-a"| G
O -.->|"independent receipt via share-backend"| E
style G fill:#e1f5fe,stroke:#0288d1,color:#1f2937
style E fill:#e8f5e9,stroke:#43a047,color:#1f2937
style P fill:#fff3e0,stroke:#fb8c00,color:#1f2937
One disposable Linux host contains two Gateway/DB containers and four network namespaces. Support reaches the Gateway on 192.0.2.254; research uses its 198.51.100.254 interface. The backend is a host Python process in share-backend, not an inference engine. PostgreSQL uses its inspected Docker-bridge address.
| Where | What happens here |
|---|---|
| Host root Bash | Create namespaces, save LAB inputs, start/stop exact owned processes. |
| share-a / share-b | Send inference as support/research; management curls use share-a. |
| share-gateway | Apply policy and forward requests; management is port 11111, inference port 8080. |
| share-backend | Read /receipt/ and /order directly, bypassing Gateway routing. |
| share-pg | Keep keys/objectives; pause only in the explicit recovery experiment. |
Walkthrough¶
Subnet match and strict versus advisory¶
Complete onboarding. Create a support subnet binding scoped to this exact inference service; do not include the management port. forward_identity requires explicit services. The resolver uses actual socket peer/ingress, not a client's asserted X-Forwarded-For. NAT that hides all clients behind one peer needs another trustworthy identity signal.
Step 1 — Create the strict support-subnet binding
Write networks-config-1.request.json with the displayed editable JSON input. Send the displayed management request to /config/ai/netidentity and retain its response.
set -euo pipefail
umask 077
source "${LAB:?set LAB to your private run directory}/env"
cat > "$LAB/networks-config-1.request.json" <<'JSON'
{
"name": "support-subnet",
"match": {
"cidr": [
"192.0.2.0/24"
]
},
"tenant": "support",
"services": [
"192.0.2.254:8080"
],
"enforce": "strict",
"priority_class": "critical",
"forward_identity": true
}
JSON
status=$(ip netns exec share-a curl -sS --max-time 10 -X POST \
-H 'Content-Type: application/json' --data-binary "@$LAB/networks-config-1.request.json" \
-D "$LAB/networks-config-1.headers" -o "$LAB/networks-config-1.response.json" \
-w '%{http_code}' "$API/config/ai/netidentity")
Step 2 — Verify the binding and list netidentity bindings
Require HTTP 200 for the saved management response before continuing. Send the displayed management request to /config/ai/netidentity and retain its response. Write subnet-support.request.json with the displayed editable JSON input.
printf 'HTTP %s\n' "$status"
jq . "$LAB/networks-config-1.response.json"
test "$status" = 200
status=$(ip netns exec share-a curl -sS --max-time 10 -X GET \
-D "$LAB/networks-config-2.headers" -o "$LAB/networks-config-2.response.json" \
-w '%{http_code}' "$API/config/ai/netidentity")
printf 'HTTP %s\n' "$status"
jq . "$LAB/networks-config-2.response.json"
test "$status" = 200
cat > "$LAB/subnet-support.request.json" <<'JSON'
{
"model": "shared-model",
"messages": [
{
"role": "user",
"content": "subnet-support"
}
],
"max_tokens": 4
}
JSON
Step 3 — Send subnet-support and retain the response
Send subnet-support through the Gateway from the namespace shown below; retain its HTTP status and response separately. Wait for the saved client PID in this same parent Bash before checking its final response. Require saved HTTP 200 for subnet-support. Read subnet-support directly from the backend receipt endpoint; this observation bypasses the Gateway. Require backend receipt count 1 for subnet-support.
ip netns exec share-a curl --http1.1 -sS --max-time 55 \
--config "$LAB/keys/support.curl" \
-H 'Content-Type: application/json' \
-H 'X-Test-Nonce: subnet-support' -H 'X-Request-Id: subnet-support' \
--data-binary "@$LAB/subnet-support.request.json" \
-D "$LAB/subnet-support.headers" -o "$LAB/subnet-support.response.json" \
-w '%{http_code}\n' "$INFER" > "$LAB/subnet-support.http"
if test -f "$LAB/subnet-support.pid"; then
wait "$(cat "$LAB/subnet-support.pid")"
fi
test "$(cat "$LAB/subnet-support.http")" = 200
ip netns exec share-backend curl -fsS --max-time 3 \
"http://127.0.0.1:8080/receipt/subnet-support" -o "$LAB/subnet-support.receipt.json"
jq -e '.count == 1' "$LAB/subnet-support.receipt.json"
jq -e '((.["headers"]["x-llm-d-inference-fairness-id"] == "support"))' "$LAB/subnet-support.receipt.json"
Step 4 — Send subnet-mismatch and retain the response
Write subnet-mismatch.request.json with the displayed editable JSON input. Send subnet-mismatch through the Gateway from the namespace shown below; retain its HTTP status and response separately. Wait for the saved client PID in this same parent Bash before checking its final response. Require saved HTTP 403 for subnet-mismatch.
cat > "$LAB/subnet-mismatch.request.json" <<'JSON'
{
"model": "shared-model",
"messages": [
{
"role": "user",
"content": "subnet-mismatch"
}
],
"max_tokens": 4
}
JSON
ip netns exec share-a curl --http1.1 -sS --max-time 55 \
--config "$LAB/keys/research.curl" \
-H 'Content-Type: application/json' \
-H 'X-Test-Nonce: subnet-mismatch' -H 'X-Request-Id: subnet-mismatch' \
--data-binary "@$LAB/subnet-mismatch.request.json" \
-D "$LAB/subnet-mismatch.headers" -o "$LAB/subnet-mismatch.response.json" \
-w '%{http_code}\n' "$INFER" > "$LAB/subnet-mismatch.http"
if test -f "$LAB/subnet-mismatch.pid"; then
wait "$(cat "$LAB/subnet-mismatch.pid")"
fi
test "$(cat "$LAB/subnet-mismatch.http")" = 403
Step 5 — Confirm no subnet-mismatch receipt; switch to advisory
Read subnet-mismatch directly from the backend receipt endpoint; this observation bypasses the Gateway. Require backend receipt count 0 for subnet-mismatch. Write networks-config-3.request.json with the displayed editable JSON input. Send the displayed management request to /config/ai/netidentity and retain its response.
ip netns exec share-backend curl -fsS --max-time 3 \
"http://127.0.0.1:8080/receipt/subnet-mismatch" -o "$LAB/subnet-mismatch.receipt.json"
jq -e '.count == 0' "$LAB/subnet-mismatch.receipt.json"
cat > "$LAB/networks-config-3.request.json" <<'JSON'
{
"name": "support-subnet",
"match": {
"cidr": [
"192.0.2.0/24"
]
},
"tenant": "support",
"services": [
"192.0.2.254:8080"
],
"enforce": "advisory",
"forward_identity": true
}
JSON
status=$(ip netns exec share-a curl -sS --max-time 10 -X POST \
-H 'Content-Type: application/json' --data-binary "@$LAB/networks-config-3.request.json" \
-D "$LAB/networks-config-3.headers" -o "$LAB/networks-config-3.response.json" \
-w '%{http_code}' "$API/config/ai/netidentity")
printf 'HTTP %s\n' "$status"
Step 6 — Send advisory-key-wins and retain the response
Require HTTP 200 for the saved management response before continuing. Write advisory-key-wins.request.json with the displayed editable JSON input. Send advisory-key-wins through the Gateway from the namespace shown below; retain its HTTP status and response separately.
jq . "$LAB/networks-config-3.response.json"
test "$status" = 200
cat > "$LAB/advisory-key-wins.request.json" <<'JSON'
{
"model": "shared-model",
"messages": [
{
"role": "user",
"content": "advisory-key-wins"
}
],
"max_tokens": 4
}
JSON
ip netns exec share-a curl --http1.1 -sS --max-time 55 \
--config "$LAB/keys/research.curl" \
-H 'Content-Type: application/json' \
-H 'X-Test-Nonce: advisory-key-wins' -H 'X-Request-Id: advisory-key-wins' \
-H 'X-llm-d-inference-objective: interactive' \
--data-binary "@$LAB/advisory-key-wins.request.json" \
-D "$LAB/advisory-key-wins.headers" -o "$LAB/advisory-key-wins.response.json" \
-w '%{http_code}\n' "$INFER" > "$LAB/advisory-key-wins.http"
Step 7 — Confirm advisory-key-wins arrived as research at standard
Wait for the saved client PID in this same parent Bash before checking its final response. Require saved HTTP 200 for advisory-key-wins. Read advisory-key-wins directly from the backend receipt endpoint; this observation bypasses the Gateway. Require backend receipt count 1 for advisory-key-wins. Write networks-config-4.request.json with the displayed editable JSON input.
if test -f "$LAB/advisory-key-wins.pid"; then
wait "$(cat "$LAB/advisory-key-wins.pid")"
fi
test "$(cat "$LAB/advisory-key-wins.http")" = 200
ip netns exec share-backend curl -fsS --max-time 3 \
"http://127.0.0.1:8080/receipt/advisory-key-wins" -o "$LAB/advisory-key-wins.receipt.json"
jq -e '.count == 1' "$LAB/advisory-key-wins.receipt.json"
jq -e '((.["headers"]["x-llm-d-inference-fairness-id"] == "research"))' "$LAB/advisory-key-wins.receipt.json"
jq -e '.headers["x-llm-d-inference-objective"] == "standard"' "$LAB/advisory-key-wins.receipt.json"
cat > "$LAB/networks-config-4.request.json" <<'JSON'
{
"name": "support-host",
"match": {
"cidr": [
"192.0.2.10/32"
]
},
"tenant": "research",
"services": [
"192.0.2.254:8080"
],
"enforce": "strict"
}
JSON
Step 8 — Add the research support-host /32 binding
Send the displayed management request to /config/ai/netidentity and retain its response. Require HTTP 200 for the saved management response before continuing. Write longest-prefix.request.json with the displayed editable JSON input.
status=$(ip netns exec share-a curl -sS --max-time 10 -X POST \
-H 'Content-Type: application/json' --data-binary "@$LAB/networks-config-4.request.json" \
-D "$LAB/networks-config-4.headers" -o "$LAB/networks-config-4.response.json" \
-w '%{http_code}' "$API/config/ai/netidentity")
printf 'HTTP %s\n' "$status"
jq . "$LAB/networks-config-4.response.json"
test "$status" = 200
cat > "$LAB/longest-prefix.request.json" <<'JSON'
{
"model": "shared-model",
"messages": [
{
"role": "user",
"content": "longest-prefix"
}
],
"max_tokens": 4
}
JSON
Step 9 — Send longest-prefix and retain the response
Send longest-prefix through the Gateway from the namespace shown below; retain its HTTP status and response separately. Wait for the saved client PID in this same parent Bash before checking its final response. Require saved HTTP 200 for longest-prefix. Read longest-prefix directly from the backend receipt endpoint; this observation bypasses the Gateway. Require backend receipt count 1 for longest-prefix.
ip netns exec share-a curl --http1.1 -sS --max-time 55 \
--config "$LAB/keys/research.curl" \
-H 'Content-Type: application/json' \
-H 'X-Test-Nonce: longest-prefix' -H 'X-Request-Id: longest-prefix' \
--data-binary "@$LAB/longest-prefix.request.json" \
-D "$LAB/longest-prefix.headers" -o "$LAB/longest-prefix.response.json" \
-w '%{http_code}\n' "$INFER" > "$LAB/longest-prefix.http"
if test -f "$LAB/longest-prefix.pid"; then
wait "$(cat "$LAB/longest-prefix.pid")"
fi
test "$(cat "$LAB/longest-prefix.http")" = 200
ip netns exec share-backend curl -fsS --max-time 3 \
"http://127.0.0.1:8080/receipt/longest-prefix" -o "$LAB/longest-prefix.receipt.json"
jq -e '.count == 1' "$LAB/longest-prefix.receipt.json"
Step 10 — Reject the equal-prefix conflicting-host binding with 409
Write networks-config-5.request.json with the displayed editable JSON input. Send the displayed management request to /config/ai/netidentity and retain its response. Require HTTP 409 for the saved management response before continuing.
cat > "$LAB/networks-config-5.request.json" <<'JSON'
{
"name": "conflicting-host",
"match": {
"cidr": [
"192.0.2.10/32"
]
},
"tenant": "support",
"services": [
"192.0.2.254:8080"
]
}
JSON
status=$(ip netns exec share-a curl -sS --max-time 10 -X POST \
-H 'Content-Type: application/json' --data-binary "@$LAB/networks-config-5.request.json" \
-D "$LAB/networks-config-5.headers" -o "$LAB/networks-config-5.response.json" \
-w '%{http_code}' "$API/config/ai/netidentity")
printf 'HTTP %s\n' "$status"
jq . "$LAB/networks-config-5.response.json"
test "$status" = 409
Step 11 — Send after-conflict and retain the response
Write after-conflict.request.json with the displayed editable JSON input. Send after-conflict through the Gateway from the namespace shown below; retain its HTTP status and response separately. Wait for the saved client PID in this same parent Bash before checking its final response. Require saved HTTP 200 for after-conflict.
cat > "$LAB/after-conflict.request.json" <<'JSON'
{
"model": "shared-model",
"messages": [
{
"role": "user",
"content": "after-conflict"
}
],
"max_tokens": 4
}
JSON
ip netns exec share-a curl --http1.1 -sS --max-time 55 \
--config "$LAB/keys/research.curl" \
-H 'Content-Type: application/json' \
-H 'X-Test-Nonce: after-conflict' -H 'X-Request-Id: after-conflict' \
--data-binary "@$LAB/after-conflict.request.json" \
-D "$LAB/after-conflict.headers" -o "$LAB/after-conflict.response.json" \
-w '%{http_code}\n' "$INFER" > "$LAB/after-conflict.http"
if test -f "$LAB/after-conflict.pid"; then
wait "$(cat "$LAB/after-conflict.pid")"
fi
test "$(cat "$LAB/after-conflict.http")" = 200
Step 12 — Confirm after-conflict delivery and delete both bindings
Read after-conflict directly from the backend receipt endpoint; this observation bypasses the Gateway. Require backend receipt count 1 for after-conflict. Send the displayed management request to /config/ai/netidentity/support-host and retain its response. Require HTTP 200 for the saved management response before continuing. Send the displayed management request to /config/ai/netidentity/support-subnet and retain its response.
ip netns exec share-backend curl -fsS --max-time 3 \
"http://127.0.0.1:8080/receipt/after-conflict" -o "$LAB/after-conflict.receipt.json"
jq -e '.count == 1' "$LAB/after-conflict.receipt.json"
status=$(ip netns exec share-a curl -sS --max-time 10 -X DELETE \
-D "$LAB/networks-config-6.headers" -o "$LAB/networks-config-6.response.json" \
-w '%{http_code}' "$API/config/ai/netidentity/support-host")
printf 'HTTP %s\n' "$status"
jq . "$LAB/networks-config-6.response.json"
test "$status" = 200
status=$(ip netns exec share-a curl -sS --max-time 10 -X DELETE \
-D "$LAB/networks-config-7.headers" -o "$LAB/networks-config-7.response.json" \
-w '%{http_code}' "$API/config/ai/netidentity/support-subnet")
printf 'HTTP %s\n' "$status"
jq . "$LAB/networks-config-7.response.json"
test "$status" = 200
Strict mismatch is 403 with receipt 0. Advisory accepts the valid credential's tenant, but the matched binding still caps its class, including a key from another tenant. An omitted priority_class defaults to standard; the advisory example requests interactive with the research key and must arrive as standard. VLAN, VXLAN and recovery bindings below also omit the field and therefore impose a standard cap. Set it explicitly when customizing authority. Longest-prefix match selects the /32; an equal-specificity conflicting update must not displace it. Prefer exact, nonoverlapping departmental networks in deployment. CIDR-only network credentials and unsupported IPv6 bindings are rejected:
Step 1 — Reject a CIDR-only network-credential binding
Write networks-config-8.request.json with the displayed editable JSON input. Send the displayed management request to /config/ai/netidentity and retain its response. Require HTTP 400 for the saved management response before continuing.
set -euo pipefail
umask 077
source "${LAB:?set LAB to your private run directory}/env"
cat > "$LAB/networks-config-8.request.json" <<'JSON'
{
"name": "bad-network-credential",
"match": {
"cidr": [
"192.0.2.0/24"
]
},
"tenant": "support",
"credential": "network"
}
JSON
status=$(ip netns exec share-a curl -sS --max-time 10 -X POST \
-H 'Content-Type: application/json' --data-binary "@$LAB/networks-config-8.request.json" \
-D "$LAB/networks-config-8.headers" -o "$LAB/networks-config-8.response.json" \
-w '%{http_code}' "$API/config/ai/netidentity")
printf 'HTTP %s\n' "$status"
jq . "$LAB/networks-config-8.response.json"
test "$status" = 400
Step 2 — Reject the IPv6 bad-v6 binding
Write networks-config-9.request.json with the displayed editable JSON input. Send the displayed management request to /config/ai/netidentity and retain its response. Require HTTP 400 for the saved management response before continuing.
cat > "$LAB/networks-config-9.request.json" <<'JSON'
{
"name": "bad-v6",
"match": {
"cidr": [
"2001:db8::/32"
]
},
"tenant": "support"
}
JSON
status=$(ip netns exec share-a curl -sS --max-time 10 -X POST \
-H 'Content-Type: application/json' --data-binary "@$LAB/networks-config-9.request.json" \
-D "$LAB/networks-config-9.headers" -o "$LAB/networks-config-9.response.json" \
-w '%{http_code}' "$API/config/ai/netidentity")
printf 'HTTP %s\n' "$status"
jq . "$LAB/networks-config-9.response.json"
test "$status" = 400
Trusted ingress interface and a narrower class cap¶
Only use credential:network when access to the named ingress is controlled by your platform. It allows keyless requests matching that interface; it does not validate an arbitrary remote user. Interface binding wins over subnet matching. The block reinstates a research subnet binding while the support interface binding is present; the keyless request must still resolve to support. It removes the overlapping subnet before testing interface-binding deletion. A presented bad credential must still fail, and a valid different-tenant key must fail under strict enforcement.
Step 1 — Reinstate support-subnet as an overlapping research binding
Send the displayed management request to /config/ai/netidentity and retain its response. Require HTTP 200 for the saved management response before continuing.
set -euo pipefail
umask 077
source "${LAB:?set LAB to your private run directory}/env"
# Reinstate a conflicting research subnet so interface precedence is observable.
jq '.tenant = "research"' "$LAB/networks-config-1.request.json" > "$LAB/interface-overlap.request.json"
status=$(ip netns exec share-a curl -sS --max-time 10 -X POST \
-H 'Content-Type: application/json' --data-binary "@$LAB/interface-overlap.request.json" \
-o "$LAB/interface-overlap.response.json" -w '%{http_code}' "$API/config/ai/netidentity")
test "$status" = 200
Step 2 — Create the support-interface network-credential binding
Write networks-config-10.request.json with the displayed editable JSON input. Send the displayed management request to /config/ai/netidentity and retain its response. Require HTTP 200 for the saved management response before continuing.
cat > "$LAB/networks-config-10.request.json" <<'JSON'
{
"name": "support-interface",
"match": {
"ingress_if": "ga"
},
"tenant": "support",
"services": [
"192.0.2.254:8080"
],
"credential": "network",
"enforce": "strict",
"forward_identity": true,
"priority_class": "standard"
}
JSON
status=$(ip netns exec share-a curl -sS --max-time 10 -X POST \
-H 'Content-Type: application/json' --data-binary "@$LAB/networks-config-10.request.json" \
-D "$LAB/networks-config-10.headers" -o "$LAB/networks-config-10.response.json" \
-w '%{http_code}' "$API/config/ai/netidentity")
printf 'HTTP %s\n' "$status"
jq . "$LAB/networks-config-10.response.json"
test "$status" = 200
sleep 3
Step 3 — Send interface-keyless and retain the response
Write interface-keyless.request.json with the displayed editable JSON input. Send interface-keyless through the Gateway from the namespace shown below; retain its HTTP status and response separately. Wait for the saved client PID in this same parent Bash before checking its final response. Require saved HTTP 200 for interface-keyless. Read interface-keyless directly from the backend receipt endpoint; this observation bypasses the Gateway.
cat > "$LAB/interface-keyless.request.json" <<'JSON'
{
"model": "shared-model",
"messages": [
{
"role": "user",
"content": "interface-keyless"
}
],
"max_tokens": 4
}
JSON
ip netns exec share-a curl --http1.1 -sS --max-time 55 \
-H 'Content-Type: application/json' \
-H 'X-Test-Nonce: interface-keyless' -H 'X-Request-Id: interface-keyless' \
--data-binary "@$LAB/interface-keyless.request.json" \
-D "$LAB/interface-keyless.headers" -o "$LAB/interface-keyless.response.json" \
-w '%{http_code}\n' "$INFER" > "$LAB/interface-keyless.http"
if test -f "$LAB/interface-keyless.pid"; then
wait "$(cat "$LAB/interface-keyless.pid")"
fi
test "$(cat "$LAB/interface-keyless.http")" = 200
ip netns exec share-backend curl -fsS --max-time 3 \
"http://127.0.0.1:8080/receipt/interface-keyless" -o "$LAB/interface-keyless.receipt.json"
Step 4 — Send interface-bad-key and retain the response
Require backend receipt count 1 for interface-keyless. Write interface-bad-key.request.json with the displayed editable JSON input. Send interface-bad-key through the Gateway from the namespace shown below; retain its HTTP status and response separately. Wait for the saved client PID in this same parent Bash before checking its final response.
jq -e '.count == 1' "$LAB/interface-keyless.receipt.json"
jq -e '((.["headers"]["x-llm-d-inference-fairness-id"] == "support"))' "$LAB/interface-keyless.receipt.json"
cat > "$LAB/interface-bad-key.request.json" <<'JSON'
{
"model": "shared-model",
"messages": [
{
"role": "user",
"content": "interface-bad-key"
}
],
"max_tokens": 4
}
JSON
ip netns exec share-a curl --http1.1 -sS --max-time 55 \
-H 'Content-Type: application/json' \
-H 'X-Test-Nonce: interface-bad-key' -H 'X-Request-Id: interface-bad-key' \
-H "@$LAB/invalid-key.headers" \
--data-binary "@$LAB/interface-bad-key.request.json" \
-D "$LAB/interface-bad-key.headers" -o "$LAB/interface-bad-key.response.json" \
-w '%{http_code}\n' "$INFER" > "$LAB/interface-bad-key.http"
if test -f "$LAB/interface-bad-key.pid"; then
wait "$(cat "$LAB/interface-bad-key.pid")"
fi
Step 5 — Send interface-wrong-tenant and retain the response
Require saved HTTP 401 for interface-bad-key. Read interface-bad-key directly from the backend receipt endpoint; this observation bypasses the Gateway. Require backend receipt count 0 for interface-bad-key. Write interface-wrong-tenant.request.json with the displayed editable JSON input. Send interface-wrong-tenant through the Gateway from the namespace shown below; retain its HTTP status and response separately.
test "$(cat "$LAB/interface-bad-key.http")" = 401
ip netns exec share-backend curl -fsS --max-time 3 \
"http://127.0.0.1:8080/receipt/interface-bad-key" -o "$LAB/interface-bad-key.receipt.json"
jq -e '.count == 0' "$LAB/interface-bad-key.receipt.json"
cat > "$LAB/interface-wrong-tenant.request.json" <<'JSON'
{
"model": "shared-model",
"messages": [
{
"role": "user",
"content": "interface-wrong-tenant"
}
],
"max_tokens": 4
}
JSON
ip netns exec share-a curl --http1.1 -sS --max-time 55 \
--config "$LAB/keys/research.curl" \
-H 'Content-Type: application/json' \
-H 'X-Test-Nonce: interface-wrong-tenant' -H 'X-Request-Id: interface-wrong-tenant' \
--data-binary "@$LAB/interface-wrong-tenant.request.json" \
-D "$LAB/interface-wrong-tenant.headers" -o "$LAB/interface-wrong-tenant.response.json" \
-w '%{http_code}\n' "$INFER" > "$LAB/interface-wrong-tenant.http"
Step 6 — Confirm interface-wrong-tenant got 403 with no receipt
Wait for the saved client PID in this same parent Bash before checking its final response. Require saved HTTP 403 for interface-wrong-tenant. Read interface-wrong-tenant directly from the backend receipt endpoint; this observation bypasses the Gateway. Require backend receipt count 0 for interface-wrong-tenant. Write interface-capped.request.json with the displayed editable JSON input.
if test -f "$LAB/interface-wrong-tenant.pid"; then
wait "$(cat "$LAB/interface-wrong-tenant.pid")"
fi
test "$(cat "$LAB/interface-wrong-tenant.http")" = 403
ip netns exec share-backend curl -fsS --max-time 3 \
"http://127.0.0.1:8080/receipt/interface-wrong-tenant" -o "$LAB/interface-wrong-tenant.receipt.json"
jq -e '.count == 0' "$LAB/interface-wrong-tenant.receipt.json"
cat > "$LAB/interface-capped.request.json" <<'JSON'
{
"model": "shared-model",
"messages": [
{
"role": "user",
"content": "interface-capped"
}
],
"max_tokens": 4
}
JSON
Step 7 — Send interface-capped and retain the response
Send interface-capped through the Gateway from the namespace shown below; retain its HTTP status and response separately. Wait for the saved client PID in this same parent Bash before checking its final response. Require saved HTTP 200 for interface-capped. Read interface-capped directly from the backend receipt endpoint; this observation bypasses the Gateway. Require backend receipt count 1 for interface-capped.
ip netns exec share-a curl --http1.1 -sS --max-time 55 \
--config "$LAB/keys/support.curl" \
-H 'Content-Type: application/json' \
-H 'X-Test-Nonce: interface-capped' -H 'X-Request-Id: interface-capped' \
-H x-llm-d-inference-objective:interactive \
--data-binary "@$LAB/interface-capped.request.json" \
-D "$LAB/interface-capped.headers" -o "$LAB/interface-capped.response.json" \
-w '%{http_code}\n' "$INFER" > "$LAB/interface-capped.http"
if test -f "$LAB/interface-capped.pid"; then
wait "$(cat "$LAB/interface-capped.pid")"
fi
test "$(cat "$LAB/interface-capped.http")" = 200
ip netns exec share-backend curl -fsS --max-time 3 \
"http://127.0.0.1:8080/receipt/interface-capped" -o "$LAB/interface-capped.receipt.json"
jq -e '.count == 1' "$LAB/interface-capped.receipt.json"
jq -e '((.["headers"]["x-llm-d-inference-objective"] == "standard"))' "$LAB/interface-capped.receipt.json"
Step 8 — Send forwarded-spoof and retain the response
Write forwarded-spoof.request.json with the displayed editable JSON input. Send forwarded-spoof through the Gateway from the namespace shown below; retain its HTTP status and response separately. Wait for the saved client PID in this same parent Bash before checking its final response. Require saved HTTP 200 for forwarded-spoof.
cat > "$LAB/forwarded-spoof.request.json" <<'JSON'
{
"model": "shared-model",
"messages": [
{
"role": "user",
"content": "forwarded-spoof"
}
],
"max_tokens": 4
}
JSON
ip netns exec share-a curl --http1.1 -sS --max-time 55 \
-H 'Content-Type: application/json' \
-H 'X-Test-Nonce: forwarded-spoof' -H 'X-Request-Id: forwarded-spoof' \
-H X-Forwarded-For:198.51.100.10 \
-H X-Auth-Tenant:research \
--data-binary "@$LAB/forwarded-spoof.request.json" \
-D "$LAB/forwarded-spoof.headers" -o "$LAB/forwarded-spoof.response.json" \
-w '%{http_code}\n' "$INFER" > "$LAB/forwarded-spoof.http"
if test -f "$LAB/forwarded-spoof.pid"; then
wait "$(cat "$LAB/forwarded-spoof.pid")"
fi
test "$(cat "$LAB/forwarded-spoof.http")" = 200
Step 9 — Confirm forwarded-spoof stayed support; delete both bindings
Read forwarded-spoof directly from the backend receipt endpoint; this observation bypasses the Gateway. Require backend receipt count 1 for forwarded-spoof. Send the displayed management request to /config/ai/netidentity/support-subnet and retain its response. Require HTTP 200 for the saved management response before continuing. Send the displayed management request to /config/ai/netidentity/support-interface and retain its response.
ip netns exec share-backend curl -fsS --max-time 3 \
"http://127.0.0.1:8080/receipt/forwarded-spoof" -o "$LAB/forwarded-spoof.receipt.json"
jq -e '.count == 1' "$LAB/forwarded-spoof.receipt.json"
jq -e '((.["headers"]["x-llm-d-inference-fairness-id"] == "support"))' "$LAB/forwarded-spoof.receipt.json"
jq -e '.headers["x-auth-tenant"] == "support"' "$LAB/forwarded-spoof.receipt.json"
# Remove the overlap before testing absence of all interface credentials.
status=$(ip netns exec share-a curl -sS --max-time 10 -X DELETE \
-o "$LAB/interface-overlap-delete.response.json" -w '%{http_code}' "$API/config/ai/netidentity/support-subnet")
test "$status" = 200
status=$(ip netns exec share-a curl -sS --max-time 10 -X DELETE \
-D "$LAB/networks-config-11.headers" -o "$LAB/networks-config-11.response.json" \
-w '%{http_code}' "$API/config/ai/netidentity/support-interface")
printf 'HTTP %s\n' "$status"
jq . "$LAB/networks-config-11.response.json"
test "$status" = 200
Step 10 — Send interface-removed and retain the response
Write interface-removed.request.json with the displayed editable JSON input. Send interface-removed through the Gateway from the namespace shown below; retain its HTTP status and response separately. Wait for the saved client PID in this same parent Bash before checking its final response. Require saved HTTP 401 for interface-removed. Read interface-removed directly from the backend receipt endpoint; this observation bypasses the Gateway.
cat > "$LAB/interface-removed.request.json" <<'JSON'
{
"model": "shared-model",
"messages": [
{
"role": "user",
"content": "interface-removed"
}
],
"max_tokens": 4
}
JSON
ip netns exec share-a curl --http1.1 -sS --max-time 55 \
-H 'Content-Type: application/json' \
-H 'X-Test-Nonce: interface-removed' -H 'X-Request-Id: interface-removed' \
--data-binary "@$LAB/interface-removed.request.json" \
-D "$LAB/interface-removed.headers" -o "$LAB/interface-removed.response.json" \
-w '%{http_code}\n' "$INFER" > "$LAB/interface-removed.http"
if test -f "$LAB/interface-removed.pid"; then
wait "$(cat "$LAB/interface-removed.pid")"
fi
test "$(cat "$LAB/interface-removed.http")" = 401
ip netns exec share-backend curl -fsS --max-time 3 \
"http://127.0.0.1:8080/receipt/interface-removed" -o "$LAB/interface-removed.receipt.json"
Step 11 — Confirm interface-removed has no backend receipt
Require backend receipt count 0 for interface-removed.
Software VLAN: tagged packets, recreation and removal¶
flowchart LR
A["share-a<br/>client110 / 100.64.11.10"] -->|"VLAN 110 on sa-ga"| G["share-gateway<br/>vlan110 / 100.64.11.254"]
G -->|"VIP 192.0.2.254:8080"| B["share-backend<br/>203.0.113.1:8080"]
style G fill:#e1f5fe,stroke:#0288d1,color:#1f2937
style B fill:#e8f5e9,stroke:#43a047,color:#1f2937
The new 100.64.11.0/24 addresses belong only to these two software VLAN interfaces. The client installs a host route to the existing service VIP through the tagged Gateway interface; the backend path is unchanged.
Create VLAN devices on both ends of the existing support veth. The Gateway alias name vlan110 is intentional: the supported VLAN binding resolves the local software device to its current ifindex. Here vlan110 is a Linux 802.1Q subinterface, not a bridge. The lab checks tagged traffic, actual link type/ifindex and resolution after recreation; it does not configure an external switch or prove physical fabric isolation.
Step 1 — Create VLAN 110 devices on the Gateway and client
Run on the Linux host. The namespace/container name selects where the operation takes effect; confirm it belongs to this lab before adapting addresses or deleting anything.
set -euo pipefail
umask 077
source "${LAB:?set LAB to your private run directory}/env"
ip netns exec share-gateway ip link add link ga name vlan110 type vlan id 110
ip netns exec share-gateway ip addr add 100.64.11.254/24 dev vlan110
ip netns exec share-gateway ip link set vlan110 up
ip netns exec share-a ip link add link sa name client110 type vlan id 110
ip netns exec share-a ip addr add 100.64.11.10/24 dev client110
ip netns exec share-a ip link set client110 up
ip netns exec share-a ip route replace 192.0.2.254/32 via 100.64.11.254
Step 2 — Create the support-vlan VLAN 110 binding
Write networks-config-12.request.json with the displayed editable JSON input. Send the displayed management request to /config/ai/netidentity and retain its response. Require HTTP 200 for the saved management response before continuing.
cat > "$LAB/networks-config-12.request.json" <<'JSON'
{
"name": "support-vlan",
"match": {
"vlan": 110
},
"tenant": "support",
"services": [
"192.0.2.254:8080"
],
"credential": "network",
"enforce": "strict",
"forward_identity": true
}
JSON
status=$(ip netns exec share-a curl -sS --max-time 10 -X POST \
-H 'Content-Type: application/json' --data-binary "@$LAB/networks-config-12.request.json" \
-D "$LAB/networks-config-12.headers" -o "$LAB/networks-config-12.response.json" \
-w '%{http_code}' "$API/config/ai/netidentity")
printf 'HTTP %s\n' "$status"
jq . "$LAB/networks-config-12.response.json"
test "$status" = 200
sleep 3
Step 3 — Send vlan-keyless and retain the response
Write vlan-keyless.request.json with the displayed editable JSON input. Send vlan-keyless through the Gateway from the namespace shown below; retain its HTTP status and response separately. Wait for the saved client PID in this same parent Bash before checking its final response. Require saved HTTP 200 for vlan-keyless. Read vlan-keyless directly from the backend receipt endpoint; this observation bypasses the Gateway.
cat > "$LAB/vlan-keyless.request.json" <<'JSON'
{
"model": "shared-model",
"messages": [
{
"role": "user",
"content": "vlan-keyless"
}
],
"max_tokens": 4
}
JSON
ip netns exec share-a curl --http1.1 -sS --max-time 55 \
-H 'Content-Type: application/json' \
-H 'X-Test-Nonce: vlan-keyless' -H 'X-Request-Id: vlan-keyless' \
--data-binary "@$LAB/vlan-keyless.request.json" \
-D "$LAB/vlan-keyless.headers" -o "$LAB/vlan-keyless.response.json" \
-w '%{http_code}\n' "$INFER" > "$LAB/vlan-keyless.http"
if test -f "$LAB/vlan-keyless.pid"; then
wait "$(cat "$LAB/vlan-keyless.pid")"
fi
test "$(cat "$LAB/vlan-keyless.http")" = 200
ip netns exec share-backend curl -fsS --max-time 3 \
"http://127.0.0.1:8080/receipt/vlan-keyless" -o "$LAB/vlan-keyless.receipt.json"
Step 4 — Send vlan-wrong-key and retain the response
Require backend receipt count 1 for vlan-keyless. Write vlan-wrong-key.request.json with the displayed editable JSON input. Send vlan-wrong-key through the Gateway from the namespace shown below; retain its HTTP status and response separately. Wait for the saved client PID in this same parent Bash before checking its final response.
jq -e '.count == 1' "$LAB/vlan-keyless.receipt.json"
jq -e '((.["headers"]["x-llm-d-inference-fairness-id"] == "support"))' "$LAB/vlan-keyless.receipt.json"
cat > "$LAB/vlan-wrong-key.request.json" <<'JSON'
{
"model": "shared-model",
"messages": [
{
"role": "user",
"content": "vlan-wrong-key"
}
],
"max_tokens": 4
}
JSON
ip netns exec share-a curl --http1.1 -sS --max-time 55 \
--config "$LAB/keys/research.curl" \
-H 'Content-Type: application/json' \
-H 'X-Test-Nonce: vlan-wrong-key' -H 'X-Request-Id: vlan-wrong-key' \
--data-binary "@$LAB/vlan-wrong-key.request.json" \
-D "$LAB/vlan-wrong-key.headers" -o "$LAB/vlan-wrong-key.response.json" \
-w '%{http_code}\n' "$INFER" > "$LAB/vlan-wrong-key.http"
if test -f "$LAB/vlan-wrong-key.pid"; then
wait "$(cat "$LAB/vlan-wrong-key.pid")"
fi
Step 5 — Confirm vlan-wrong-key denial and recreate vlan110
Require saved HTTP 403 for vlan-wrong-key and backend receipt count 0, read directly from the backend. Then delete and recreate vlan110 on the Gateway and require a new interface index, so the next step tests the binding against a recreated device.
test "$(cat "$LAB/vlan-wrong-key.http")" = 403
ip netns exec share-backend curl -fsS --max-time 3 \
"http://127.0.0.1:8080/receipt/vlan-wrong-key" -o "$LAB/vlan-wrong-key.receipt.json"
jq -e '.count == 0' "$LAB/vlan-wrong-key.receipt.json"
ip netns exec share-gateway ip -s -d link show vlan110
ip netns exec share-gateway ip -j link show vlan110 > "$LAB/vlan-before.json"
ip netns exec share-gateway ip link del vlan110
ip netns exec share-gateway ip link add link ga name vlan110 type vlan id 110
ip netns exec share-gateway ip addr add 100.64.11.254/24 dev vlan110
ip netns exec share-gateway ip link set vlan110 up
sleep 4
ip netns exec share-gateway ip -j link show vlan110 > "$LAB/vlan-after.json"
test "$(jq '.[0].ifindex' "$LAB/vlan-before.json")" != "$(jq '.[0].ifindex' "$LAB/vlan-after.json")"
Step 6 — Send vlan-recreated and retain the response
Write vlan-recreated.request.json with the displayed editable JSON input. Send vlan-recreated through the Gateway from the namespace shown below; retain its HTTP status and response separately. Wait for the saved client PID in this same parent Bash before checking its final response. Require saved HTTP 200 for vlan-recreated. Read vlan-recreated directly from the backend receipt endpoint; this observation bypasses the Gateway.
cat > "$LAB/vlan-recreated.request.json" <<'JSON'
{
"model": "shared-model",
"messages": [
{
"role": "user",
"content": "vlan-recreated"
}
],
"max_tokens": 4
}
JSON
ip netns exec share-a curl --http1.1 -sS --max-time 55 \
-H 'Content-Type: application/json' \
-H 'X-Test-Nonce: vlan-recreated' -H 'X-Request-Id: vlan-recreated' \
--data-binary "@$LAB/vlan-recreated.request.json" \
-D "$LAB/vlan-recreated.headers" -o "$LAB/vlan-recreated.response.json" \
-w '%{http_code}\n' "$INFER" > "$LAB/vlan-recreated.http"
if test -f "$LAB/vlan-recreated.pid"; then
wait "$(cat "$LAB/vlan-recreated.pid")"
fi
test "$(cat "$LAB/vlan-recreated.http")" = 200
ip netns exec share-backend curl -fsS --max-time 3 \
"http://127.0.0.1:8080/receipt/vlan-recreated" -o "$LAB/vlan-recreated.receipt.json"
Step 7 — Confirm vlan-recreated delivery and delete support-vlan
Require backend receipt count 1 for vlan-recreated. Send the displayed management request to /config/ai/netidentity/support-vlan and retain its response. Require HTTP 200 for the saved management response before continuing. Write vlan-no-binding.request.json with the displayed editable JSON input.
jq -e '.count == 1' "$LAB/vlan-recreated.receipt.json"
status=$(ip netns exec share-a curl -sS --max-time 10 -X DELETE \
-D "$LAB/networks-config-13.headers" -o "$LAB/networks-config-13.response.json" \
-w '%{http_code}' "$API/config/ai/netidentity/support-vlan")
printf 'HTTP %s\n' "$status"
jq . "$LAB/networks-config-13.response.json"
test "$status" = 200
cat > "$LAB/vlan-no-binding.request.json" <<'JSON'
{
"model": "shared-model",
"messages": [
{
"role": "user",
"content": "vlan-no-binding"
}
],
"max_tokens": 4
}
JSON
Step 8 — Send vlan-no-binding and retain the response
Send vlan-no-binding through the Gateway from the namespace shown below; retain its HTTP status and response separately. Wait for the saved client PID in this same parent Bash before checking its final response. Require saved HTTP 401 for vlan-no-binding. Read vlan-no-binding directly from the backend receipt endpoint; this observation bypasses the Gateway. Require backend receipt count 0 for vlan-no-binding.
ip netns exec share-a curl --http1.1 -sS --max-time 55 \
-H 'Content-Type: application/json' \
-H 'X-Test-Nonce: vlan-no-binding' -H 'X-Request-Id: vlan-no-binding' \
--data-binary "@$LAB/vlan-no-binding.request.json" \
-D "$LAB/vlan-no-binding.headers" -o "$LAB/vlan-no-binding.response.json" \
-w '%{http_code}\n' "$INFER" > "$LAB/vlan-no-binding.http"
if test -f "$LAB/vlan-no-binding.pid"; then
wait "$(cat "$LAB/vlan-no-binding.pid")"
fi
test "$(cat "$LAB/vlan-no-binding.http")" = 401
ip netns exec share-backend curl -fsS --max-time 3 \
"http://127.0.0.1:8080/receipt/vlan-no-binding" -o "$LAB/vlan-no-binding.receipt.json"
jq -e '.count == 0' "$LAB/vlan-no-binding.receipt.json"
ip netns exec share-a ip route del 192.0.2.254/32
ip netns exec share-a ip link del client110
ip netns exec share-gateway ip link del vlan110
Expect RX packets on vlan110, a changed ifindex after recreation, successful keyless traffic after refresh, then receipt 0 after deleting the binding. A stored alias alone is not a packet-path check.
VXLAN: encapsulated client and underlay failure¶
flowchart LR
B["share-b<br/>client221 / 100.64.22.10"] -->|"VNI 221 / UDP 4789<br/>underlay 198.51.100.10 to .254"| G["share-gateway<br/>vxlan221 / 100.64.22.254"]
A["share-a<br/>ordinary keyed control"] -->|"unchanged sa-ga path"| G
G --> E["share-backend<br/>203.0.113.1:8080"]
style G fill:#e1f5fe,stroke:#0288d1,color:#1f2937
style E fill:#e8f5e9,stroke:#43a047,color:#1f2937
The overlay uses 100.64.22.0/24; the existing research veth network is its underlay. Only the overlay device is interrupted. Keep the independent support path as a normal control while testing research recovery.
Create a VNI 221 overlay over the research underlay, then interrupt only the Gateway's overlay device. Key-based support traffic must remain healthy. Match vni resolves the Gateway-local vxlan221 device; this is node-local configuration, so configure and verify each serving node separately.
Step 1 — Create the VNI 221 VXLAN devices
Run on the Linux host. The namespace/container name selects where the operation takes effect; confirm it belongs to this lab before adapting addresses or deleting anything.
set -euo pipefail
umask 077
source "${LAB:?set LAB to your private run directory}/env"
ip netns exec share-gateway ip link add vxlan221 type vxlan id 221 local 198.51.100.254 remote 198.51.100.10 dstport 4789 nolearning
ip netns exec share-gateway ip addr add 100.64.22.254/24 dev vxlan221
ip netns exec share-gateway ip link set vxlan221 up
ip netns exec share-b ip link add client221 type vxlan id 221 local 198.51.100.10 remote 198.51.100.254 dstport 4789 nolearning
ip netns exec share-b ip addr add 100.64.22.10/24 dev client221
ip netns exec share-b ip link set client221 up
ip netns exec share-b ip route replace 192.0.2.254/32 via 100.64.22.254
Step 2 — Create the research-overlay VNI 221 binding
Write networks-config-14.request.json with the displayed editable JSON input. Send the displayed management request to /config/ai/netidentity and retain its response. Require HTTP 200 for the saved management response before continuing.
cat > "$LAB/networks-config-14.request.json" <<'JSON'
{
"name": "research-overlay",
"match": {
"vni": 221
},
"tenant": "research",
"services": [
"192.0.2.254:8080"
],
"credential": "network",
"enforce": "strict",
"forward_identity": true
}
JSON
status=$(ip netns exec share-a curl -sS --max-time 10 -X POST \
-H 'Content-Type: application/json' --data-binary "@$LAB/networks-config-14.request.json" \
-D "$LAB/networks-config-14.headers" -o "$LAB/networks-config-14.response.json" \
-w '%{http_code}' "$API/config/ai/netidentity")
printf 'HTTP %s\n' "$status"
jq . "$LAB/networks-config-14.response.json"
test "$status" = 200
sleep 3
Step 3 — Send vxlan-keyless and retain the response
Write vxlan-keyless.request.json with the displayed editable JSON input. Send vxlan-keyless through the Gateway from the namespace shown below; retain its HTTP status and response separately. Wait for the saved client PID in this same parent Bash before checking its final response. Require saved HTTP 200 for vxlan-keyless. Read vxlan-keyless directly from the backend receipt endpoint; this observation bypasses the Gateway.
cat > "$LAB/vxlan-keyless.request.json" <<'JSON'
{
"model": "shared-model",
"messages": [
{
"role": "user",
"content": "vxlan-keyless"
}
],
"max_tokens": 4
}
JSON
ip netns exec share-b curl --http1.1 -sS --max-time 55 \
-H 'Content-Type: application/json' \
-H 'X-Test-Nonce: vxlan-keyless' -H 'X-Request-Id: vxlan-keyless' \
--data-binary "@$LAB/vxlan-keyless.request.json" \
-D "$LAB/vxlan-keyless.headers" -o "$LAB/vxlan-keyless.response.json" \
-w '%{http_code}\n' "$INFER" > "$LAB/vxlan-keyless.http"
if test -f "$LAB/vxlan-keyless.pid"; then
wait "$(cat "$LAB/vxlan-keyless.pid")"
fi
test "$(cat "$LAB/vxlan-keyless.http")" = 200
ip netns exec share-backend curl -fsS --max-time 3 \
"http://127.0.0.1:8080/receipt/vxlan-keyless" -o "$LAB/vxlan-keyless.receipt.json"
Step 4 — Send vxlan-wrong-key and retain the response
Require backend receipt count 1 for vxlan-keyless. Write vxlan-wrong-key.request.json with the displayed editable JSON input. Send vxlan-wrong-key through the Gateway from the namespace shown below; retain its HTTP status and response separately. Wait for the saved client PID in this same parent Bash before checking its final response.
jq -e '.count == 1' "$LAB/vxlan-keyless.receipt.json"
jq -e '((.["headers"]["x-llm-d-inference-fairness-id"] == "research"))' "$LAB/vxlan-keyless.receipt.json"
cat > "$LAB/vxlan-wrong-key.request.json" <<'JSON'
{
"model": "shared-model",
"messages": [
{
"role": "user",
"content": "vxlan-wrong-key"
}
],
"max_tokens": 4
}
JSON
ip netns exec share-b curl --http1.1 -sS --max-time 55 \
--config "$LAB/keys/support.curl" \
-H 'Content-Type: application/json' \
-H 'X-Test-Nonce: vxlan-wrong-key' -H 'X-Request-Id: vxlan-wrong-key' \
--data-binary "@$LAB/vxlan-wrong-key.request.json" \
-D "$LAB/vxlan-wrong-key.headers" -o "$LAB/vxlan-wrong-key.response.json" \
-w '%{http_code}\n' "$INFER" > "$LAB/vxlan-wrong-key.http"
if test -f "$LAB/vxlan-wrong-key.pid"; then
wait "$(cat "$LAB/vxlan-wrong-key.pid")"
fi
Step 5 — Confirm vxlan-wrong-key denial and take vxlan221 down
Require saved HTTP 403 for vxlan-wrong-key and backend receipt count 0, read directly from the backend. Then take vxlan221 down on the Gateway; the probe from share-b is expected to fail at the transport level, so its error is saved separately. Write overlay-other-path.request.json with the displayed editable JSON input.
test "$(cat "$LAB/vxlan-wrong-key.http")" = 403
ip netns exec share-backend curl -fsS --max-time 3 \
"http://127.0.0.1:8080/receipt/vxlan-wrong-key" -o "$LAB/vxlan-wrong-key.receipt.json"
jq -e '.count == 0' "$LAB/vxlan-wrong-key.receipt.json"
ip netns exec share-gateway ip -s -d link show vxlan221
ip netns exec share-gateway ip link set vxlan221 down
# No HTTP response is expected; save the transport error separately.
ip netns exec share-b curl -sS --max-time 2 -o "$LAB/vxlan-down.body" -w '%{http_code}\n' http://192.0.2.254:8080/v1/models > "$LAB/vxlan-down.code" 2> "$LAB/vxlan-down.stderr" && exit 1
cat > "$LAB/overlay-other-path.request.json" <<'JSON'
{
"model": "shared-model",
"messages": [
{
"role": "user",
"content": "overlay-other-path"
}
],
"max_tokens": 4
}
JSON
Step 6 — Send overlay-other-path and retain the response
Send overlay-other-path through the Gateway from the namespace shown below while vxlan221 is down; retain its HTTP status and response separately. Require saved HTTP 200 and backend receipt count 1, read directly from the backend. Finally bring vxlan221 back up.
ip netns exec share-a curl --http1.1 -sS --max-time 55 \
--config "$LAB/keys/support.curl" \
-H 'Content-Type: application/json' \
-H 'X-Test-Nonce: overlay-other-path' -H 'X-Request-Id: overlay-other-path' \
--data-binary "@$LAB/overlay-other-path.request.json" \
-D "$LAB/overlay-other-path.headers" -o "$LAB/overlay-other-path.response.json" \
-w '%{http_code}\n' "$INFER" > "$LAB/overlay-other-path.http"
if test -f "$LAB/overlay-other-path.pid"; then
wait "$(cat "$LAB/overlay-other-path.pid")"
fi
test "$(cat "$LAB/overlay-other-path.http")" = 200
ip netns exec share-backend curl -fsS --max-time 3 \
"http://127.0.0.1:8080/receipt/overlay-other-path" -o "$LAB/overlay-other-path.receipt.json"
jq -e '.count == 1' "$LAB/overlay-other-path.receipt.json"
ip netns exec share-gateway ip link set vxlan221 up
sleep 3
Step 7 — Send vxlan-recovered and retain the response
Write vxlan-recovered.request.json with the displayed editable JSON input. Send vxlan-recovered through the Gateway from the namespace shown below; retain its HTTP status and response separately. Wait for the saved client PID in this same parent Bash before checking its final response. Require saved HTTP 200 for vxlan-recovered. Read vxlan-recovered directly from the backend receipt endpoint; this observation bypasses the Gateway.
cat > "$LAB/vxlan-recovered.request.json" <<'JSON'
{
"model": "shared-model",
"messages": [
{
"role": "user",
"content": "vxlan-recovered"
}
],
"max_tokens": 4
}
JSON
ip netns exec share-b curl --http1.1 -sS --max-time 55 \
-H 'Content-Type: application/json' \
-H 'X-Test-Nonce: vxlan-recovered' -H 'X-Request-Id: vxlan-recovered' \
--data-binary "@$LAB/vxlan-recovered.request.json" \
-D "$LAB/vxlan-recovered.headers" -o "$LAB/vxlan-recovered.response.json" \
-w '%{http_code}\n' "$INFER" > "$LAB/vxlan-recovered.http"
if test -f "$LAB/vxlan-recovered.pid"; then
wait "$(cat "$LAB/vxlan-recovered.pid")"
fi
test "$(cat "$LAB/vxlan-recovered.http")" = 200
ip netns exec share-backend curl -fsS --max-time 3 \
"http://127.0.0.1:8080/receipt/vxlan-recovered" -o "$LAB/vxlan-recovered.receipt.json"
Step 8 — Confirm vxlan-recovered delivery and delete research-overlay
Require backend receipt count 1 for vxlan-recovered. Send the displayed management request to /config/ai/netidentity/research-overlay and retain its response. Require HTTP 200 for the saved management response before continuing. Write vxlan-no-binding.request.json with the displayed editable JSON input.
jq -e '.count == 1' "$LAB/vxlan-recovered.receipt.json"
status=$(ip netns exec share-a curl -sS --max-time 10 -X DELETE \
-D "$LAB/networks-config-15.headers" -o "$LAB/networks-config-15.response.json" \
-w '%{http_code}' "$API/config/ai/netidentity/research-overlay")
printf 'HTTP %s\n' "$status"
jq . "$LAB/networks-config-15.response.json"
test "$status" = 200
cat > "$LAB/vxlan-no-binding.request.json" <<'JSON'
{
"model": "shared-model",
"messages": [
{
"role": "user",
"content": "vxlan-no-binding"
}
],
"max_tokens": 4
}
JSON
Step 9 — Send vxlan-no-binding and retain the response
Send vxlan-no-binding through the Gateway from the namespace shown below; retain its HTTP status and response separately. Wait for the saved client PID in this same parent Bash before checking its final response. Require saved HTTP 401 for vxlan-no-binding. Read vxlan-no-binding directly from the backend receipt endpoint; this observation bypasses the Gateway. Require backend receipt count 0 for vxlan-no-binding.
ip netns exec share-b curl --http1.1 -sS --max-time 55 \
-H 'Content-Type: application/json' \
-H 'X-Test-Nonce: vxlan-no-binding' -H 'X-Request-Id: vxlan-no-binding' \
--data-binary "@$LAB/vxlan-no-binding.request.json" \
-D "$LAB/vxlan-no-binding.headers" -o "$LAB/vxlan-no-binding.response.json" \
-w '%{http_code}\n' "$INFER" > "$LAB/vxlan-no-binding.http"
if test -f "$LAB/vxlan-no-binding.pid"; then
wait "$(cat "$LAB/vxlan-no-binding.pid")"
fi
test "$(cat "$LAB/vxlan-no-binding.http")" = 401
ip netns exec share-backend curl -fsS --max-time 3 \
"http://127.0.0.1:8080/receipt/vxlan-no-binding" -o "$LAB/vxlan-no-binding.receipt.json"
jq -e '.count == 0' "$LAB/vxlan-no-binding.receipt.json"
ip netns exec share-b ip route del 192.0.2.254/32
ip netns exec share-b ip link del client221
ip netns exec share-gateway ip link del vxlan221
If a path fails, inspect routes, rp_filter, local device name/ifindex, alias refresh and actual packets before diagnosing tenant policy. A nonexistent interface may be accepted with a warning while matching no traffic. Do not substitute a GET-success claim for traffic attribution. Continue to dispatch.
Check your result¶
Use strict/advisory mismatch, tagged/encapsulated packets, device recreation and keyless receipt evidence separately.
Keep the configured policy, HTTP status, resolved identity, and independent backend receipt together. A rejected request must have receipt 0; a successful request must have receipt 1. A missing receipt response is a failed observation, never a zero.
Next steps and cleanup¶
Continue in the documented order with the same LAB. Remove an experiment's binding or temporary rule before the next experiment; stop the shared lab only at the final cleanup chapter. Read the lab starting point if required values are missing. Use cleanup and recovery when ending the run.