Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,19 @@ WARN k.d.s.core.UrlPolicy : ssrf-guard: Host not allowed: evil.com (reason=block

태그가 bounded — Prometheus / Datadog / CloudWatch 문제 없음.

## 성능

인터셉터의 allowed 경로 비용은 **요청당 ~5 μs** (JMH, JDK 21) — 100 ms 외부 API 호출 대비 **0.005% 오버헤드**, 실질적으로 invisible.

| Hot path | 평균 비용 | 비고 |
|---|---:|---|
| `UrlPolicy.validate` allowed | ~5 μs | 프로덕션 트래픽의 99%+ |
| `UrlPolicy.validate` blocked | 5-12 μs | early-exit (IP 리터럴)이 late-exit (whitelist)보다 저렴 |
| `JsonToolInputGuard` 작은 JSON | ~6 μs | URL 하나짜리 LLM 툴 입력 |
| `JsonToolInputGuard` ~2 KB JSON | ~24 μs | URL 3개 있는 RAG-augmented 툴 입력 |

전체 방법론, stdev 포함 케이스별 숫자, 해석 가이드는 [`BENCHMARKS.md`](./BENCHMARKS.md). 직접 재현: `./gradlew :ssrf-guard-benchmarks:jmh`.

## 자동 구성이 등록하는 빈 (RestClient 모듈)

`ssrf.guard.enabled=true`(기본값)이면 RestClient 자동설정이 활성화되어 다음을 등록:
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,19 @@ WARN k.d.s.core.UrlPolicy : ssrf-guard: Host not allowed: evil.com (reason=block

Tags are bounded (`reason` is an enum, `scheme` is http/https) — Prometheus / Datadog / CloudWatch ingest happily.

## Performance

The interceptor's allowed-path cost is **~5 μs per request** (JMH, JDK 21) — **0.005% overhead** on a 100 ms remote API call, invisible in practice.

| Hot path | Avg cost | Notes |
|---|---:|---|
| `UrlPolicy.validate` allowed | ~5 μs | 99%+ of production traffic |
| `UrlPolicy.validate` blocked | 5-12 μs | Cheaper if early-exit (IP literal) than late-exit (whitelist) |
| `JsonToolInputGuard` small JSON | ~6 μs | LLM tool input with one URL |
| `JsonToolInputGuard` ~2 KB JSON | ~24 μs | RAG-augmented tool input with 3 URLs |

Full methodology, per-case numbers with stdev, and how to read them: see [`BENCHMARKS.md`](./BENCHMARKS.md). Reproduce yourself with `./gradlew :ssrf-guard-benchmarks:jmh`.

## What auto-configuration registers (RestClient module)

When `ssrf.guard.enabled=true` (the default), the RestClient autoconfig activates and registers:
Expand Down
73 changes: 73 additions & 0 deletions docs/guides/performance.ko.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# 성능

인터셉터의 allowed 경로 비용은 JDK 21 기준 **요청당 ~5 μs** — 100 ms 외부 API 호출 대비 0.005% 오버헤드, 실질적으로 invisible. 아래 숫자들은 실제 사용자가 비용을 지불하는 hot path에 대한 JMH 마이크로벤치마크. 로컬 재현: `./gradlew :ssrf-guard-benchmarks:jmh`.

## 무엇을 측정하나

| 모듈 | 메서드 | 왜 중요한가 |
|---|---|---|
| `ssrf-guard-core` | `UrlPolicy.validate(URI)` | 모든 HTTP 클라이언트 인터셉터 (RestClient / RestTemplate / WebClient / Feign / OkHttp)가 요청마다 한 번 호출 |
| `ssrf-guard-llm` | `JsonToolInputGuard.checkOrFormatError(json)` | `-springai`와 `-langchain4j`가 LLM 툴 호출마다 한 번 호출 |

DNS 시점 게이트 (`-httpclient5`의 `SafeDnsResolver`, OkHttp `Dns`, `-webclient`의 reactor-netty `AddressResolverGroup`)는 마이크로벤치마크 대상 아님 — DNS 해석은 네트워크 I/O가 지배. 모킹된 resolver로 측정해봐야 모킹을 측정하는 셈.

## 결과

JMH 1.37, JDK 21, single-fork, 5×1s warmup + 5×1s measurement.

### `UrlPolicy.validate(URI)` — 요청당

| 벤치마크 | Score | Error | 비고 |
|---|---:|---:|---|
| `allowed` | 5,285 ns | ± 924 ns | 해피 패스 — 프로덕션 트래픽의 99%+ |
| `blockedIpLiteral` | 4,888 ns | ± 750 ns | allowed보다 **빠름** — IP 리터럴 검사가 화이트리스트 lookup보다 먼저 |
| `blockedHost` | 11,822 ns | ± 2,035 ns | 가장 비싼 차단 경로 — 전체 체인 + `SsrfGuardException` |

### `JsonToolInputGuard` — LLM 툴 호출당

| 벤치마크 | Score | Error | 비고 |
|---|---:|---:|---|
| `small_allowed` | 5,629 ns | ± 486 ns | `{"url": "..."}` — `fetch_url` 류 툴의 floor 비용 |
| `medium_blocked` | 6,722 ns | ± 290 ns | URL이 2단계 깊이로 중첩, IP 리터럴 검사에서 차단 |
| `large_allowed` | 24,228 ns | ± 4,958 ns | ~2 KB JSON, URL 3개 + 비 URL 필드 40개 (RAG 형태) |

JSON 가드는 대략 `Jackson 파싱 + N × UrlPolicy.validate` 스케일.

## 실용적 해석

| 본인 요청 레이턴시 | ssrf-guard `allowed` 비용 | 오버헤드 |
|---|---|---|
| 1 ms (클러스터 내부 호출) | ~5 μs | ~0.5% |
| 10 ms (리전 내부 호출) | ~5 μs | ~0.05% |
| 100 ms (외부 API) | ~5 μs | ~0.005% |
| 500 ms (LLM 툴 호출) | ~5 μs | ~0.001% |

LLM 에이전트가 툴 호출 (LLM 왕복 + 실제 fetch 포함 100 ms - 5 s end-to-end) 하나 할 때, `JsonToolInputGuard`는 **~6-24 μs** 추가 — LLM 비용 대비 invisible.

## 숫자 해석 가이드

- **"steady-state 오버헤드가 얼마?" 라는 질문엔 `allowed` 인용**. 차단 경로 비용은 거부 시점에는 실재하지만 production에서는 드묾 — 차단된 요청은 설정 미스 또는 실제 공격, 일반적이지 않음.
- **실제 요청 레이턴시와 비교**. 5 μs라도 어떤 out-of-process 호출의 network jitter noise floor 안에 들어감.
- **`JsonToolInputGuard`와 `UrlPolicy`를 직접 비교 금지** — JSON 가드는 Jackson 파싱 + 트리 walk + 차단 페이로드 직렬화 (거부 시). URL-time 인터셉터와는 apples-to-oranges.
- **차단 경로가 허용 경로보다 비쌈** — 예외 생성이 측정에 포함됨. 현실적임 — 차단 비용은 공격을 막는 대가.

## 측정 안 한 것

의도적으로 범위 밖 (그리고 이 숫자에서 추론하면 안 되는 것):

- **DNS 시점 게이트** — I/O 지배; 모킹 없이 마이크로벤치마크 의미 없음
- **Spring 자동 설정 시작 비용** — 앱 부트 시 1회 비용, 요청당 비용 아님
- **Micrometer 메트릭 오버헤드** — 벤치마크는 `NoOpSsrfGuardMetrics` 사용해서 가드 자체 비용 격리. 실제 Micrometer는 `Counter.increment()`당 ~50 ns 추가
- **네이티브 이미지 (GraalVM) 성능** — 다른 형태; 위의 JIT 숫자가 상한. AOT 비교는 `nativeRun` 아래에서 같은 JMH harness 실행.

## 재현

```bash
./gradlew :ssrf-guard-benchmarks:jmh
```

출력: `ssrf-guard-benchmarks/build/results/jmh/results.txt`.

정식 숫자 (블로그 글이나 커밋 메시지용)는 [`ssrf-guard-benchmarks/build.gradle.kts`](https://github.com/devslab-kr/ssrf-guard/blob/main/ssrf-guard-benchmarks/build.gradle.kts)에서 `fork = 3`으로 올려서 JIT 분산 캡처.

정식 원본 — 방법론, 새 벤치마크 기여 가이드 — [`BENCHMARKS.md`](https://github.com/devslab-kr/ssrf-guard/blob/main/BENCHMARKS.md).
73 changes: 73 additions & 0 deletions docs/guides/performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Performance

The allowed-path cost of the interceptor is **~5 μs per request** on JDK 21 — 0.005% overhead on a 100 ms remote API call, invisible in practice. The numbers below come from JMH micro-benchmarks against the actual hot paths consumers pay for. Reproduce locally with `./gradlew :ssrf-guard-benchmarks:jmh`.

## What's measured

| Module | Method | Why it matters |
|---|---|---|
| `ssrf-guard-core` | `UrlPolicy.validate(URI)` | What every HTTP-client interceptor (RestClient / RestTemplate / WebClient / Feign / OkHttp) invokes once per request |
| `ssrf-guard-llm` | `JsonToolInputGuard.checkOrFormatError(json)` | What `-springai` and `-langchain4j` invoke once per LLM tool call |

The DNS-time gates (`SafeDnsResolver` in `-httpclient5`, OkHttp `Dns`, reactor-netty `AddressResolverGroup` in `-webclient`) aren't micro-benchmarked here — DNS resolution is dominated by network I/O, not by the policy code that runs around it. Measuring with a mock resolver would just measure the mock.

## Results

JMH 1.37, JDK 21, single-fork, 5×1s warmup + 5×1s measurement.

### `UrlPolicy.validate(URI)` — per-request

| Benchmark | Score | Error | Notes |
|---|---:|---:|---|
| `allowed` | 5,285 ns | ± 924 ns | The happy path — what 99%+ of production traffic costs |
| `blockedIpLiteral` | 4,888 ns | ± 750 ns | **Faster** than allowed — IP-literal check fires before whitelist lookup |
| `blockedHost` | 11,822 ns | ± 2,035 ns | The most expensive blocked path — full chain + `SsrfGuardException` |

### `JsonToolInputGuard` — per-LLM-tool-call

| Benchmark | Score | Error | Notes |
|---|---:|---:|---|
| `small_allowed` | 5,629 ns | ± 486 ns | `{"url": "..."}` — floor cost for a `fetch_url`-style tool |
| `medium_blocked` | 6,722 ns | ± 290 ns | Nested URL two levels deep, blocked at IP-literal check |
| `large_allowed` | 24,228 ns | ± 4,958 ns | ~2 KB JSON, 3 URLs + 40 non-URL fields (RAG-shaped) |

The JSON guard scales roughly as `Jackson parse + N × UrlPolicy.validate`.

## Practical takeaway

| Your request latency | ssrf-guard `allowed` cost | Overhead |
|---|---|---|
| 1 ms (in-cluster call) | ~5 μs | ~0.5% |
| 10 ms (in-region call) | ~5 μs | ~0.05% |
| 100 ms (remote API) | ~5 μs | ~0.005% |
| 500 ms (LLM tool call) | ~5 μs | ~0.001% |

For an LLM-driven agent making a tool call (100 ms - 5 s end-to-end including the LLM round-trip), `JsonToolInputGuard` adds **~6-24 μs**, which is invisible against the LLM cost.

## How to read these numbers

- **Quote `allowed` when answering "what's the steady-state overhead?"**. The blocked-path costs are realistic on rejection but rare — a blocked request in production is a misconfiguration or an actual attack, not the common case.
- **Compare against your real request latency**. Even at 5 μs per check, that's well inside the noise floor of network jitter on any out-of-process call.
- **Don't compare `JsonToolInputGuard` to `UrlPolicy` directly** — the JSON guard parses Jackson + walks the tree + serialises a block payload on rejection. Apples-to-oranges with the URL-time interceptor.
- **The block path is more expensive than the allow path** for both benchmarks, because exception construction is in the measurement. This is realistic — paying for a block is the price of stopping an attack.

## What this doesn't measure

Deliberately out of scope (and what would be wrong to extrapolate from these numbers):

- **DNS-time gates** — dominated by I/O; meaningless to micro-benchmark without mocking
- **Spring auto-configuration startup cost** — one-time tax at app boot, not per-request
- **Micrometer metrics overhead** — benchmarks use `NoOpSsrfGuardMetrics` to isolate the guard's own cost. Real Micrometer adds ~50 ns per `Counter.increment()` on top
- **Native-image (GraalVM) performance** — different shape; JIT numbers above are an upper bound. Run the same JMH harness under `nativeRun` for AOT.

## Reproducing

```bash
./gradlew :ssrf-guard-benchmarks:jmh
```

Output lands at `ssrf-guard-benchmarks/build/results/jmh/results.txt`.

For canonical numbers (e.g. a blog post or a commit message), bump `fork = 3` in [`ssrf-guard-benchmarks/build.gradle.kts`](https://github.com/devslab-kr/ssrf-guard/blob/main/ssrf-guard-benchmarks/build.gradle.kts) to capture JIT variance.

Full canonical source — methodology in narrative form, contribution guide for new benchmarks — at [`BENCHMARKS.md`](https://github.com/devslab-kr/ssrf-guard/blob/main/BENCHMARKS.md).
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ plugins:
Guides: 가이드
Security Model: 보안 모델
Configuration: 설정
Performance: 성능
Changelog: 변경 이력
Contributing: 기여하기

Expand Down Expand Up @@ -120,5 +121,6 @@ nav:
- Guides:
- Security Model: guides/security-model.md
- Configuration: guides/configuration.md
- Performance: guides/performance.md
- Changelog: changelog.md
- Contributing: contributing.md