# URL의 IPv6 zone은 실수

> Clean Markdown view of GeekNews topic #30201. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=30201](https://news.hada.io/topic?id=30201)
- GeekNews Markdown: [https://news.hada.io/topic/30201.md](https://news.hada.io/topic/30201.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-06-05T15:01:53+09:00
- Updated: 2026-06-05T15:01:53+09:00
- Original source: [xeiaso.net](https://xeiaso.net/notes/2026/ipv6-zones-go-url/)
- Points: 1
- Comments: 1

## Topic Body

- **IPv6 zone**은 여러 인터페이스가 같은 `fe80::` 링크 로컬 범위를 쓸 때 `fe80::4%eth0`처럼 대상 인터페이스를 구분하게 하는 표기임
- URL에서 IPv6 주소는 포트와 콜론을 구분하려고 `[fe80::4]:80`처럼 감싸며, zone까지 붙이면 **대괄호 표기**가 `[fe80::4%eth0]:80` 형식이 됨
- Go의 `net/url`은 `]:80`을 `%et`라는 잘못된 URL escape로 해석해 **invalid URL escape** 오류를 냄
- RFC 6874는 zone이 있는 IPv6 리터럴을 `IPv6address "%25" ZoneID`로 정의하므로, URL에서는 `%`를 **`%25`** 로 percent-encode해야 함
- Anubis가 IPv6 zone 주소를 가리키려면 이 표기를 써야 하며, 브라우저·nginx·Requests 관련 이슈처럼 origin과 라이브러리 호환성까지 걸린 **엣지 케이스**로 남음

---

### IPv6 zone와 URL 문법 충돌
- IPv6의 링크 로컬 주소는 인터페이스마다 `fe80::whatever` 범위에 놓일 수 있어, 두 네트워크 인터페이스가 있을 때 `fe80::4` 목적지를 구분하려면 [IPv6 scope/zone](<https://en.wikipedia.org/wiki/IPv6_address#Scoped_literal_IPv6_addresses_(with_zone_index>)) 사용이 필요함
- zone 값의 형식은 운영체제마다 다르며, Linux에서는 인터페이스 이름, Windows에서는 인터페이스 ID를 사용함
- 예시에서 `eth0`는 이더넷 장치 이름이며, 주소는 다음처럼 표현됨

```text
fe80::4%eth0
```

- 호스트와 포트는 보통 콜론으로 구분하지만 IPv6 주소도 헥스 그룹 구분에 콜론을 쓰므로, 포트 80의 `fe80::4`는 다음처럼 대괄호로 감싸야 함

```text
[fe80::4]:80
```

- zone까지 붙이면 다음 형식이 됨

```text
[fe80::4%eth0]:80
```

- 이를 URL 호스트명에 직접 넣은 `http://[fe80::4%eth0]:80`은 Go `net/url`에서 `%et`를 잘못된 URL escape로 해석해 실패함

```text
panic: parse "http://[fe80::4%eth0]:80": invalid URL escape "%et"
```

### 표준상 해결책과 남은 문제
- URL 문법에 맞지 않는 값은 [percent-encoding](https://en.wikipedia.org/wiki/Percent-encoding)을 사용해야 하며, URL의 `%20`은 URL에서 유효하지 않은 ASCII 공백을 인코딩한 예임
- IPv6 zone의 `%` 자체도 인코딩해야 하므로, Go에서는 `http://[fe80::4%25eth0]:80`처럼 써야 `Hostname()` 결과가 `fe80::4%eth0`로 나옴
- [RFC 9844](https://www.rfc-editor.org/rfc/rfc9844.txt)는 사용자 인터페이스에서 IPv6 zone을 다루는 지침을 제공하며, [RFC 6874](https://www.rfc-editor.org/rfc/rfc6874.html)는 URL의 zone 포함 IPv6 리터럴 문법을 다음처럼 정의함

```text
IP-literal = "[" ( IPv6address / IPv6addrz / IPvFuture  ) "]"

ZoneID = 1*( unreserved / pct-encoded )

IPv6addrz = IPv6address "%25" ZoneID
```

- 같은 엣지 케이스는 [nginx 티켓](https://trac.nginx.org/nginx/ticket/623), [Requests 이슈](https://github.com/psf/requests/issues/6808), [HTTP link-local URI BCP 초안](https://datatracker.ietf.org/doc/html/draft-schinazi-httpbis-link-local-uri-bcp-03)에도 걸려 있음
- 브라우저는 현재 IPv6 zone을 지원하지 않으며, 이유는 많은 미묘한 동작에 쓰이는 “origin” 개념을 깨뜨리기 때문이고, 해당 초안은 브라우저가 사용할 수 있도록 IPv6의 zone origin을 정의하려는 시도임
- Anubis가 IPv6 zone 주소를 가리키려면 `%`를 percent-encoding해야 하며, Go 표준 라이브러리를 포크하지 않는 정책 아래에서는 이 엣지 케이스의 나쁜 UX를 감수해야 함

## Comments



### Comment 58977

- Author: neo
- Created: 2026-06-05T15:01:54+09:00
- Points: 1

###### [Lobste.rs 의견들](https://lobste.rs/s/lcnc5e/ipv6_zones_urls_are_mistake) 
- `TL;DR: 컴퓨터는 실수였다`는 식의 결론은 **목욕물 버리다 아기까지 버리는** 느낌 아닌가 싶음  
  말 그대로 Trigun 같은 애니 악당 논리처럼, 인간이 끔찍한 범죄를 저지를 수 있으니 모든 인간을 없애겠다는 식임  
  농담 섞인 표현인 건 알지만 흥미롭고, 지금 준비 중인 다음 발표 주제로 삼을 예정임
  - 제목이 아주 극적이고 **낚시성**인 건 맞음  
    그래도 핵심은 공감됨. 이런 상황 자체가 꽤 우스꽝스럽지 않나 싶음
- URL에서 **링크-로컬 범위 IPv6 주소**를 제대로 처리하지 못하는 건 실수임  
  다만 링크-로컬 IPv6 주소를 제대로 다루지 못하는 경우가 드물지는 않음
  - 반대로 URL 안의 IPv6 주소에서 **존(zone)** 을 처리하려면 복잡도가 꽤 늘어남  
    이제 `%`가 URL의 호스트 부분에서만, 그것도 호스트가 IPv6 주소이고 `[...]` 안에 있을 때만 다른 의미를 갖기 때문임  
    문법이 여전히 모호하지 않을 수는 있지만, 핵심은 그게 아님. 이런 예외 사례가 많아질수록 URL 파서가 특정 예외를 놓칠 가능성이 커지고, 파서 간 차이에서 지저분한 버그나 보안 문제가 숨어들기 쉬움  
    개인적으로는 URL에서 IPv6 존을 처리하는 쪽을 선호하지만, 한때 `%`를 URL 인코딩해야 한다는 지침이 있었던 만큼 지금 와서 되돌리면 실제로 모호성이 생김. 아쉬운 일임
- [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986)와 호환되는 라이브러리나 구현이라면 이 문제를 만나게 됨. 명세가 퍼센트 인코딩된 시퀀스를 이렇게 정의하기 때문임  
  `pct-encoded = "%" HEXDIG HEXDIG`  
  여기서 `HEXDIG`는 `[a-fA-F]`로 정의되어 있어서 `%et`를 잘못된 시퀀스로 파싱함  
  명세는 또 `%` 문자가 퍼센트 인코딩된 옥텟의 표시자로 쓰이므로, URI 안에서 데이터로 쓰려면 `%25`로 퍼센트 인코딩해야 한다고 말함. 이미 디코딩된 문자열을 다시 디코딩하면 퍼센트 데이터 옥텟을 퍼센트 인코딩의 시작으로 오해할 수 있고, 이미 인코딩된 문자열을 다시 인코딩해도 반대 문제가 생기므로 구현은 같은 문자열을 두 번 이상 인코딩하거나 디코딩하면 안 된다고 함  
  그래서 짜증나는 건 맞지만, 실제로는 **버그라기 어렵다**고 봄
- 존이 있는 주소에 `%`를 직접 쓰는 게 왜 그렇게 끔찍한가 싶음. 인코딩된 문자는 호스트 부분에서도 허용되고, 존 주소에 `%`를 그대로 쓰면 **모호성**이 생김  
  `%` 자체는 예약되지 않은 문자가 아니므로 퍼센트 인코딩하는 게 맞음  
  Go의 `net/url`과 `net/http`가 URL RFC와 충돌하는 건 새삼스러운 일이 아님. 특히 `net/url.URL.Path`의 존재와 `net/http`에서의 사용이 꽤 성가신데, `%2F`를 깨뜨리기 때문임. `net/http.Redirect`도 `path.Clean`을 사용해서 `//`를 `/`로 잘못 접어버림  
  Go 표준 라이브러리에서 URL을 만지는 부분을 포크하고 싶거나 `net/url/v2` 같은 걸 제안하고 싶은 이유는 많음. 하지만 이 글에서 보이는 한, Go의 **IPv6 존 주소 처리**는 타당하고 올바른 편으로 보임
