# clumsy

> 윈도우 PC의 인터넷을 일부러 나쁘게 만드는 도구입니다. PC 게임의 끊김·지연 테스트에 쓰려고 열었고 소스 2,709줄을 다 읽었습니다. 판정은 채택인데, 같은 걸 8월에 이미 분석했고 두 번 다 한 번도 안 돌렸다는 게 이 편의 교훈입니다(2026-10-02).

- 수치: 수확 2.5 · 실행 0회
- 사람이 보는 화면: https://nobles92ts-ship-it.github.io/ko/teardowns/qa/clumsy/
- 반대 언어: https://nobles92ts-ship-it.github.io/en/teardowns/qa/clumsy/index.md
- 저장소: https://github.com/jagt/clumsy
- 이 사이트 안내(AI용): https://nobles92ts-ship-it.github.io/ko/llms.txt

---

**clumsy는 윈도우 PC의 인터넷을 일부러 나쁘게 만드는 무료 프로그램입니다. 제가 QA하는 PC 게임이 끊기거나 느려질 때 제대로 버티는지 보려고 열었습니다. 판정은 「쓴다」인데, 이 편의 진짜 이야기는 따로 있습니다 — 같은 레포를 8월에 이미 한 번 분석했고, 두 번 다 한 번도 돌려 보지 않았습니다.**

> **[도판]** 소포를 집는 것은 윈도우 드라이버이고, clumsy는 그 위에 얹은 조작판이다. 그래서 이 PC 밖의 기기에는 손이 닿지 않는다.
>
> 게임이 서버와 주고받는 소포는 윈도우 네트워크 길목의 WinDivert 갈고리를 지난다. 갈고리가 필터에 맞는 소포를 집어 clumsy 조작판에 넘기면, 조작판이 늦추기·버리기·끊기·속도 제한을 한 뒤 다시 내보낸다. 필터에 안 맞는 소포는 그냥 지나간다. 켜는 법은 창에서 체크하거나 명령줄 한 줄이다. 이 PC가 직접 주고받는 소포만 잡고 다른 기기의 소포에는 손이 닿지 않는다.

## 먼저 — 「인터넷을 나쁘게 만든다」는 게 무슨 뜻인가요

인터넷으로 오가는 데이터는 한 덩어리로 가지 않습니다. **작은 소포 수십 개로 잘려서** 오가고, 이 소포 하나를 「패킷」이라고 부릅니다. 게임은 1초에도 수십 개씩 서버와 패킷을 주고받습니다.

clumsy는 PC의 인터넷 출입문에 서서 이 소포들을 붙잡습니다. 그리고 시킨 대로 괴롭힙니다.

| 시키는 것 | 비유하면 | 게임에서는 |
|---|---|---|
| **늦추기** | 소포를 문 앞에 잠깐 세워 둠 | 핑이 높아짐 — 누르고 한참 뒤에 반응 |
| **몇 개 버리기** | 소포 몇 개를 쓰레기통에 | 캐릭터가 순간이동하듯 튐 |
| **전부 버리기** | 출입문을 아예 닫음 | 서버와 연결이 끊김 |
| **속도 제한** | 한 번에 지나가는 양을 묶음 | 로딩이 길어짐 |

이 밖에 순서 섞기·복사하기·내용 망가뜨리기·연결 강제로 끊기까지 모두 여덟 가지가 있습니다. 게임은 켜 둔 채로 창에서 숫자만 바꾸면 됩니다.

## 왜 이게 필요한가요 — 강제 종료로는 안 지나가는 길이 있습니다

게임 QA에는 「접속이 끊겼다가 다시 들어오면 데이터가 맞나」를 보는 일이 많습니다. 보통은 게임을 강제로 꺼 버리고 다시 켜서 확인합니다.

그런데 강제 종료는 게임 자체가 죽습니다. 실제 사용자가 겪는 끊김은 다릅니다 — **게임은 살아 있는데 선만 끊깁니다.** 이때 게임은 「연결이 끊겼네 → 다시 붙어 보자 → 재연결 창을 띄우자」라는 별도의 길을 타는데, 강제 종료로는 이 길을 한 번도 지나가지 않습니다.

랜선을 뽑으면 되긴 합니다. 하지만 메신저도 원격 접속도 같이 끊깁니다. clumsy는 **게임 서버로 가는 소포만 골라서** 막을 수 있고, 그게 이 도구를 쓰는 이유입니다.

## 소스를 열어 보니, 설명서에 없는 것이 넷 있었습니다

clumsy는 C로 2,709줄입니다. 작아서 전부 읽었습니다. 설명서와 다르거나 설명서에 아예 없는 것이 넷 나왔습니다.

1. **지연은 40ms 단위로 흔들립니다.** 안쪽 시계가 40ms마다 한 번 돌기 때문이고, 그래서 100ms를 넣으면 실제로는 100~140ms 사이가 됩니다. 「조금 느림」과 「많이 느림」을 가르는 데는 충분합니다. 몇 ms를 재는 데는 못 씁니다.
2. **왕복에는 두 번 걸립니다.** 나가는 소포와 들어오는 소포에 같은 값이 붙어서, 핑을 300 늘리고 싶으면 150을 넣어야 합니다.
3. **명령줄로 켤 땐 관리자 창에서 켜야 합니다.** 일반 창에서 켜면 오류 메시지 하나 없이 그냥 꺼집니다.
4. **Stop을 누르고 나서 닫아야 합니다.** 창을 바로 닫거나 정해 둔 시간이 다 돼서 꺼지면, 붙잡고 있던 소포를 내보내지 않고 버린 채 끝납니다.

## 제 환경에 대 보니 — PC 게임에는 맞고, 휴대폰에는 안 닿습니다

clumsy는 **그 PC가 직접 주고받는 소포만** 잡습니다. 그래서 PC 게임 테스트에는 그대로 쓸 수 있고, 휴대폰 실기기 테스트에는 못 씁니다. 휴대폰의 소포는 PC를 지나가지 않으니까요.

쓸 자리도 이미 있었습니다. 제 TC 생성 파이프라인이 만든 테스트 설계 문서 30편을 훑어보니, 재현 방법 칸에 「네트워크 차단 도구」가 24번, 「지연 주입」이 38번 적혀 있었습니다. 어떤 도구인지는 한 번도 적혀 있지 않았습니다. 그리고 제 [TC 파이프라인](/ko/built/tc-team/index.md) 규칙은 재현 방법이 없는 케이스를 아예 빼 버립니다. clumsy가 그 빈칸에 들어갈 이름입니다.

## 그런데 이건 두 번째 분석이었습니다

리포트를 다 쓰고 목록에 올리려다 알았습니다. **8월 18일에 같은 레포를 이미 분석해 뒀습니다.** 판정도 같았습니다 — PC에는 쓴다, 휴대폰은 안 된다.

처음 검색에서 못 찾은 이유는 단순합니다. 옛 리포트는 HTML 파일인데, 검색 대상에서 HTML을 뺐습니다.

더 아픈 건 그다음입니다. 6주 동안 한 번도 돌려 보지 않았고, 8월 리포트가 「제일 먼저 할 1분짜리 확인」이라고 적어 둔 것도 손대지 않은 채 그대로 남아 있었습니다.

→ 배운 것: **분석을 한 번 더 해도 판정은 움직이지 않았습니다. 판정을 움직일 수 있는 건 5분짜리 실측뿐이었습니다.** 같은 대상을 다시 열기 전에, 지난번에 「다음에 할 일」로 적어 둔 것부터 했는지 봐야 했습니다.

## 다음에 할 일 — 이번엔 순서까지 적어 둡니다

| 순서 | 할 일 | 걸리는 시간 |
|---|---|---|
| 1 | 게임 엔진에 들어 있는 네트워크 흉내 기능을 콘솔에서 켜 보기 | 1분 · 설치 없음 |
| 2 | 회사 PC에서 clumsy를 받아 핑으로 확인하기 | 5분 |
| 3 | 실제 테스트 케이스 하나를 clumsy로 재현해 보기 | 30분쯤 |

1번이 먹히면 게임 쪽 지연 테스트는 그걸로 충분하고 clumsy는 「완전히 끊기」 전용이 됩니다. 2번에서 백신이 막으면 거기서 멈춥니다. 보안 설정을 풀면서까지 쓰지는 않습니다.

## 여기부터는 자세한 기록입니다

**판정은 PC 쪽 채택(조건부)이고, 같은 판정이 8월에 이미 나와 있었다는 것이 이 편의 사건입니다.** clumsy는 윈도우 커널 드라이버 WinDivert 위에 GUI 한 장을 얹은 C 프로그램입니다. 필터에 맞는 패킷을 붙잡아 늦추고·버리고·복제하고·섞고·변조하고·RST를 꽂고·대역폭을 깎은 뒤 다시 주입합니다. 2013년에 나와 별 6,251개를 모았고, 2022년 1월 이후 master 커밋이 없습니다. 저는 PC 게임 클라이언트의 「끊김·지연」 예외 케이스를 앱 수정 없이 재현할 수단으로 열었습니다.

## 무엇을 하는 물건인가 — 2,709줄짜리 패킷 조작판

| 항목 | 실측 (2026-10-02) |
|---|---|
| 규모 | C 소스 14파일 **2,709줄** · 별 6,251 · 포크 620 |
| 활동 | master 마지막 커밋 2022-01-12 · 최신 릴리스 0.3(2023-10-21) |
| 배포 | 같은 0.3을 **a·b·c 세 벌**로 낸다 — WinDivert 드라이버 서명이 서로 다르다. win64 누적 다운로드 a 400,186 · b 12,811 · c 18,856 |
| 의존 | WinDivert 2.2.0 동봉(상류 최신은 2.2.2) · GUI는 IUP |
| 라이선스 | MIT. GitHub 표기는 NOASSERTION인데, LICENSE 첫 줄에 붙은 설명문 탓으로 보인다 |
| 열린 이슈+PR | 131 |

## 어떻게 동작하나 — 실 두 개와 40ms 시계

동작은 실(thread) 두 개가 나눠 맡습니다. 하나는 `WinDivertRecv` 로 패킷을 받을 때마다 목록에 넣고, 모듈 여덟을 차례로 통과시킨 뒤 남은 것을 내보냅니다. 다른 하나는 **40ms마다**(`CLOCK_WAITMS 40`) 같은 일을 한 번 더 합니다. 붙잡아 둔 패킷은 다음 패킷이 오거나 이 시계가 돌 때만 풀립니다.

| 모듈 | 하는 일 | 소스에서 본 것 |
|---|---|---|
| Lag | 고정 지연 0–15,000ms | 들어오기·나가기에 **같은 값 하나**(#172). 2,000개가 쌓이면 800개를 즉시 내보냄 |
| Drop | 패킷마다 확률로 폐기 | 확률을 0~10000 정수로 저장(0.01% 단위). 독립 시행이라 몰아서 잃는 모양은 안 나옴 |
| Throttle | 시간 창 동안 모았다가 한꺼번에 내보냄 | 「Drop Throttled」를 켜면 묶음째 폐기 — 몰아서 잃는 손실은 이걸로만 흉내 낸다 |
| Duplicate · OOD | 2–50개 복제 · 최대 10 차례 뒤로 미루기 | — |
| Tamper | 페이로드 비트 변조 | 체크섬 재계산이 기본 ON이라, 변조된 데이터가 앱까지 도착한다 |
| Reset | TCP RST 플래그 세팅 | TCP 헤더가 있을 때만 — UDP에는 효과 없음 |
| Bandwidth | KB/s 상한 | 줄을 세우지 않고 **초과분을 폐기**. 들어오기·나가기가 계량기 `rateStats` **하나를 공유**한다 |

## 대표 설계 결정 — 그리고 그게 성립하는 조건

| 결정 | 성립 조건 |
|---|---|
| 커널 드라이버로 OS 길목에서 가로챈다 | 관리자 권한 · 드라이버 서명 통과 · 보안 제품이 막지 않을 것. 레이어가 `WINDIVERT_LAYER_NETWORK` 라 **이 PC 자신의 트래픽만** 잡는다 |
| 필터 문법으로 대상을 고른다 | 서버 IP·포트를 알아야 한다. 이 레이어에는 프로세스 정보가 없어서 **프로그램 이름으로는 못 거른다** |
| 시계 하나를 40ms로 돌린다 | 트래픽이 뜸하면 설정값보다 방향마다 0~40ms 늦다. 100ms 단위 체감 테스트에는 무해하다 |
| 명령줄은 `--키 값` 을 전역 저장소에 넣고 모듈이 꺼내 쓴다 | 관리자가 아니면 `tryElevate` 의 silent 분기가 **메시지 없이 종료**한다. 승격 재실행(runas)은 인자를 넘기지 않는다 |

## 깨본 결과 — 문서와 소스가 갈리는 다섯 곳

1. **지연 정확도.** #87(열림)은 지연을 0으로 둬도 연속 요청 간격이 80ms, 곧 방향마다 40ms 벌어지고, 1ms 설정에서는 50ms까지 튄다고 보고합니다. 소스의 40ms 시계와 정확히 맞물립니다. 그래서 이 값은 「설정값」으로만 적고 실측 왕복 시간으로 인용하지 않습니다.
2. **종료 경로.** `--timeout` 이 끝나거나 창을 닫으면 `IUP_CLOSE` → `cleanup()` 으로 가는데, 여기서 `divertStop()` 을 부르지 않습니다. 필터는 프로세스가 끝나며 풀리지만 Lag가 붙잡고 있던 패킷은 내보내지 않고 사라집니다. 모듈을 닫고 남은 패킷을 다 보내는 길은 Stop 버튼 하나뿐입니다.
3. **명령줄 무음 종료.** #141 「옵션을 주면 즉시 꺼짐」의 원인은 위 silent 분기로 보입니다(추정 — 실행으로 확인하지 않았습니다).
4. **문서 어긋남.** 위키는 지연 범위를 0–3,000으로 적었지만 코드는 15,000까지 받습니다. `--timeout`·`--duplicate-count` 는 위키에 아예 없습니다(#189).
5. **환경.** 백신이 0.3 zip을 `Trojan:Script/Ulthar.A!ml` 로 탐지(#187, 2025-11, 열림) · 드라이버 서명 오류(#84) · 닫은 뒤에도 느려짐이 남음(#31, 재부팅으로 해결) · 블루스크린 보고 1건(#107) · 원격 제어와 단축키 없음(#1, 2013년부터 열림).

> **[도판]** 같은 대상을 두 번 뜯었는데 칸 배치는 그대로였다. 칸을 옮길 수 있는 것은 실측뿐이다.
>
> 판정을 세 칸으로 나눈 그림. 쓴다 칸에는 특정 서버만 완전히 끊기, 100ms 이상 단위의 지연, timeout으로 자동 복구가 있고 PC 게임과 테스트 계정으로만 쓴다. 미뤄 둔다 칸에는 TCP 연결 강제로 끊기와 확률로 버리기·속도 제한이 있고 판정 기준이 생기면 다시 본다. 못 쓴다 칸에는 휴대폰 실기기 트래픽과 몇 ms 단위의 정밀 측정이 있다. 판정은 8월 분석과 같고, 두 번 분석했지만 돌린 것은 0번이다.

## 내 환경에 대봤다 — 빈칸 62곳, 그리고 한 겹 겹치는 대안

제가 QA하는 것은 PC 클라이언트와 안드로이드 실기기 두 갈래입니다. clumsy가 닿는 건 PC 쪽뿐입니다.

PC 쪽에는 빈칸이 있었습니다. 제 TC 파이프라인이 낸 테스트 설계 문서 30편에서, 재현 수단 자리에 「네트워크 차단 도구」 24회 · 「지연 주입」 38회 · 「비행기 모드」 5회가 도구 이름 없이 적혀 있었습니다(2026-10-02 grep). 파이프라인 규칙은 동시성·경합 케이스를 **재현 수단이 확보된 경우에만** 쓰고, 나머지는 「재현 수단 필요 항목」으로 빼 둡니다. 수단 하나가 생기면 빠지던 케이스가 돌아옵니다.

겹치는 대안은 셋입니다.

| 대안 | clumsy와의 관계 |
|---|---|
| 프로세스 강제 종료 → 재접속 | **대체가 아니라 보완.** 클라이언트가 죽으니 「끊김 감지 → 재연결 → 재연결 팝업」 경로를 아예 안 탄다 |
| 엔진 내장 네트워크 흉내 (예: 언리얼 엔진의 `NetEmulation.PktLag`) | **게임 채널의 지연·손실은 겹친다.** 드라이버도 관리자 권한도 필요 없고 더 정밀하다. 다만 게임 채널에만 걸리고, 출하용 빌드에는 없고, 깔끔한 완전 단절은 어렵다 |
| 랜선 뽑기 | 메신저·원격 접속까지 같이 끊기고, 원격으로만 쓰는 PC에서는 할 수 없다 |

8월 분석은 사내 빌드의 디버그 심볼에서 엔진 쪽 패킷 시뮬레이션 이름들을 찾아, 이 기능이 켜진 채 컴파일됐을 정황이 강하다고 적었습니다. 그 뒤로 콘솔에 직접 쳐 보는 1분짜리 확인은 아무도 하지 않았습니다.

## 수확과 판정

| 기법 | 판정 |
|---|---|
| 서버 IP 필터 + Drop 100% = 그 서버만 완전 단절 | ✅ **채택** — PC 끊김 예외 케이스의 재현 수단 |
| Lag 100ms 이상 | ✅ **조건부(0.5)** — 엔진 내장이 먹히면 그쪽이 우선. 값은 「설정값」으로만 적는다 |
| `--timeout N` | ✅ **채택** — N초 끊김을 반복 재현하고, 원격 PC에서 스스로를 고립시키지 않게 막는 안전줄 |
| Reset (TCP RST) | ⏸ **보류** — 어떤 연결이 TCP인지 아직 모른다 |
| 확률 손실 · 대역폭 | ⏸ **보류** — 손실 모양이 실제 회선과 다르고 합격선이 없다 |
| 안드로이드 실기기 | ⛔ **기각** — PC를 지나가지 않는다. 핫스팟용 포크(`abaza121/clumsy-hotspot`)는 폰을 잡는 대신 PC 자신을 못 잡는다 |

되돌리기 비용은 사실상 0입니다. Stop 한 번이면 끝나니, 신뢰도가 중간이어도 해 보는 쪽이 맞습니다. 멈출 신호도 미리 정해 둡니다 — a·b·c 세 벌이 다 서명이나 백신에 막히면 거기서 끝내고, 예외 등록이나 서명 검사 끄기는 하지 않습니다.

## 못 한 것

- **실행 실측 0회.** 커널 드라이버를 올려야 해서 이번에도 돌리지 않았습니다. 위 숫자는 전부 소스와 이슈에서 읽은 것입니다.
- **엔진 내장 기능 확인 0회.** 1분이면 되는데 8월부터 남아 있습니다.
- **회사 보안 정책 확인 안 함.** 백신이 막는지는 받아 봐야 압니다.
- **휴대폰 쪽 수단은 이번에도 비어 있습니다.**

**이 편에서 값을 치른 교훈은 순서입니다.** 같은 대상을 두 번 분석했는데 판정은 한 글자도 안 바뀌었고, 바뀐 건 쓰는 법이 늘고 소스에서 함정 넷을 더 찾은 것뿐이었습니다. 판정을 움직일 수 있었던 건 처음부터 5분짜리 실측 하나였는데, 그건 두 번 다 「다음에」로 남았습니다. 그리고 두 번째 분석이 시작된 것 자체가 **검색 범위에서 HTML을 뺀 탓**이었습니다 — 「없다」를 한 자리만 보고 말한 값입니다.
