3P by GN⁺ | ★ favorite | 댓글 1개
  • OpenVPN을 WireGuard로 바꾸는 기본 절차는 단순하지만, 실제 운영에서는 NAT 우회IPv6 공개 주소 할당 같은 추가 구성이 필요해짐
  • 기본 VPN은 서버와 클라이언트의 키쌍, 10.100.0.0/16 같은 VPN 대역, wg0iptables NAT 규칙, 클라이언트별 AllowedIPs로 구성됨
  • WireGuard는 UPnP 서버를 포함하지 않는 단순한 커널 모듈이므로, BitTorrent처럼 NAT에 민감한 애플리케이션은 서버에 miniupnpd를 붙여 포트 포워딩을 처리해야 함
  • IPv6에서는 호스팅 제공자가 준 /48 또는 서버의 /56 범위 안에서 VPN 클라이언트에 공개 라우팅 가능한 IPv6 주소를 직접 줄 수 있음
  • 클라이언트가 2001:aaaa:bbbb:1000:cafe::2/128 같은 공개 IPv6를 받으면 인터넷에서 직접 포트에 접근할 수 있어, UPnP 기반 주소 감지나 포트 포워딩이 필요 없어짐

WireGuard 기본 VPN 구성

  • 목표는 호스팅 제공자 서버를 VPN 서버로 쓰고, 기존 OpenVPN 구성을 WireGuard로 대체하는 것임
  • 기본 구성 흐름은 서버와 클라이언트의 키쌍을 만든 뒤 VPN 대역을 정하는 방식임
    • 서버용 개인키/공개키 키쌍 생성
    • 각 클라이언트용 키쌍 생성
    • VPN 네트워크 선택
      • 예시 VPN 대역: 10.100.0.0/16
      • 서버 IP: 10.100.0.1
      • 클라이언트 IP: 10.100.0.2, 10.100.0.3
  • 서버 설정은 wg0 인터페이스에 주소, 개인키, 리슨 포트, NAT/포워딩용 iptables 규칙을 넣음
[Interface]
Address = 10.100.0.1/24
PrivateKey = (redacted)
ListenPort = 51820
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o ens0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o ens0 -j MASQUERADE
[Peer]
PublicKey = (redacted)
AllowedIPs = 10.100.0.2/32
[Peer]
PublicKey = (redacted)
AllowedIPs = 10.100.0.3/32
  • 서버는 wg up /etc/wireguard/wg0.conf로 시작함
  • 클라이언트 설정에는 서버 공개키, 엔드포인트, 전체 트래픽 라우팅용 AllowedIPs = 0.0.0.0/0, ::/0이 들어감
[Interface]
PrivateKey = (redacted)
[Peer]
PublicKey = (redacted)
Endpoint = my-server.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
  • 선택적으로 VPN용 네트워크 네임스페이스를 만들면, 기본 인터넷 연결은 직접 유지하면서 특정 애플리케이션만 VPN 네임스페이스 안에서 실행할 수 있음

NAT와 UPnP가 필요한 이유

  • BitTorrent 클라이언트 같은 일부 애플리케이션은 NAT 뒤에서 동작이 불안정할 수 있음
  • WireGuard 여부와 관계없이 VPN은 NAT처럼 동작하며, UPnP는 이 문제를 우회하는 흔한 방법임
  • UPnP가 해결하는 문제는 크게 두 가지임
    • 컴퓨터가 외부 시스템에서 보이는 공인 주소를 모를 수 있음
      • VPN 뒤에서는 공인 주소가 ISP가 할당한 주소가 아니라 VPN 서버 주소임
      • VPN 소프트웨어는 그 주소를 알지만, 시스템의 다른 부분은 일반적으로 알지 못함
      • P2P 소프트웨어가 이 주소를 모르면 다른 피어에게 전달할 수 없음
    • 포트를 열어도 연결은 클라이언트가 아니라 VPN 서버 IP의 해당 포트로 들어옴
      • 예시는 TCP 8043
      • 실제로 클라이언트에 도달하려면 VPN 서버의 포트 포워딩 규칙이 필요함
      • UPnP는 이 포트 포워딩을 설정하는 수단이 됨
  • 단순 WireGuard 구성에서는 miniupnpcexternal-ip 도구가 UPnP 장치를 찾지 못함
$ external-ip
No IGD UPnP Device found on the network !

WireGuard에 miniupnpd 붙이기

  • WireGuard는 단순한 커널 모듈이며 UPnP 서버를 포함하지 않음
  • 서버에 miniupnpd를 설치하고, WireGuard 설정의 PostUp/PostDown에서 서비스를 시작·중지하도록 구성함
PostUp = systemctl start miniupnpd
PostDown = systemctl stop miniupnpd
  • 핵심 설정 파일은 /etc/miniupnpd/miniupnpd.conf
  • 필요한 주요 설정은 다음과 같음
    • ext_ifname=ens0
      • 서버의 인터넷-facing 인터페이스임
      • 실제 환경에서는 ens0이 아닐 수 있음
    • listening_ip=wg0
      • 서버의 WireGuard 네트워크 인터페이스임
    • uuid=06df7440-dbac-404c-9c07-0b0dbfca609e
      • uuidgen으로 생성할 수 있음
      • 모든 동작이 private, non-routable 네트워크 안에서 일어나므로 예시 UUID를 써도 문제없다고 봄
    • allow 1024-65535 10.100.0.0/16 1024-65535
      • WireGuard 네트워크 10.100.0.0/16에 대해 허용 범위를 지정함

UPnP 동작 확인

  • external-ip는 서버 IP를 반환하고, upnpc는 서버의 TCP 8043을 내부 10.100.0.2:8043으로 리다이렉트함
$ external-ip
(redacted, but it correctly returned my server IP)
$ upnpc -n 10.100.0.2 8043 8043 tcp 300
external (redacted:server-ip):8043 TCP is redirected to internal 10.100.0.2:8043 (duration=300)
$ socat TCP-LISTEN:8043 STDIO
  • 다른 머신에서 서버 IP의 TCP 8043으로 접속하면 두 socat 인스턴스가 VPN을 거쳐 통신할 수 있음
$ socat TCP:(redacted:server-ip):8043 STDIO

IPv6에서는 NAT 없이 구성 가능

  • NAT 문제를 UPnP로 우회하는 것보다 더 나은 방식은 NAT 자체를 없애는 것이며, IPv6에서는 이 구성이 가능함
  • WireGuard 기반 VPN의 IPv6 튜토리얼은 IPv4 구성을 그대로 옮기는 경우가 있음
    • IPv4의 10.100.0.0/16 같은 private, non-routable 네트워크를 IPv6의 fd00:dead:beef::/48 같은 대역으로 바꿈
    • 서버와 클라이언트에 이 네트워크 안의 주소를 할당함
    • ip6tables masquerade 액션을 추가함
  • 여기서는 그 대신 공개 라우팅 가능한 IPv6를 클라이언트에 직접 할당함
  • 예시 환경에서는 호스팅 제공자가 계정에 /48 네트워크 2001:aaaa:bbbb::/48를 할당했고, 서버에는 /562001:aaaa:bbbb:1000::1/56이 있음
  • VPN 네트워크에는 /80 서브네트워크 2001:aaaa:bbbb:1000:cafe::/80을 할당함
[Interface]
Address = 2001:aaaa:bbbb:1000:cafe::1/80
[Peer]
AllowedIPs = 2001:aaaa:bbbb:1000:cafe::2/128
[Peer]
AllowedIPs = 2001:aaaa:bbbb:1000:cafe::3/128
  • 클라이언트 쪽에는 해당 공개 IPv6 주소를 직접 지정함
[Interface]
Address = 2001:aaaa:bbbb:1000:cafe::2/128
  • 서버에서 ip -6 route get 2001:aaaa:bbbb:1000:cafe::2가 WireGuard 인터페이스 wg0를 반환해야 함
    • 그렇지 않으면 라우팅에서 wg0에 더 낮은 metric을 줘야 함
  • 클라이언트는 IPv6에서 직접 포트를 리슨할 수 있음
$ socat TCP6-LISTEN:8043
  • 인터넷의 다른 위치에서 클라이언트 IPv6 주소로 직접 접속 가능함
$ socat TCP6:[2001:aaaa:bbbb:1000:cafe::2]:8043
  • 기본 라우트 장치의 IP 주소가 2001:aaaa:bbbb:1000:cafe::2가 되므로, IPv4에서 private 주소였던 VPN 인터페이스 IP가 IPv6에서는 공개 IP가 됨
  • 이 구성에서는 UPnP로 공개 IPv6를 감지하거나 포트 포워딩을 설정할 필요가 없음

댓글과 토론

Hacker News 의견들
  • WireGuard 설정할 때 https://www.procustodibus.com/blog/2020/10/wireguard-topolog...가 아주 유용했음

    • WireGuard 문제가 잘못 쓰고 있는 냄새가 날 때마다 이 글 묶음으로 돌아오게 됨
      각 구성별 상세 글 링크도 놓치면 안 됨
    • 이 블로그는 여러 시나리오에서 매우 유용한 자료가 됨
  • 동적 DNS 레코드로 호스트명을 집 네트워크의 동적 IP에 매핑해두면 사설 VPN이 실제로 쓸 만해짐
    로컬 서비스와 리소스를 공용 인터넷에 노출하지 않고도 밖에서 접근할 수 있어 완전히 게임 체인저임

    • 내부 서비스와 외부 서비스 중 뭘 쓰는지 궁금함
      Cloudflare도 잘 동작했고, PXE/네트워크 부팅을 만지다가 Tailscale로 옮겼지만 Tailscale의 Magic DNS 너머로 뭘 쓸지는 아직 못 정했음
      Unbound도 꽤 좋아 보임
    • 예를 들면 https://freedns.afraid.org/dynamic/와 라우터의 cron 작업으로 DNS 레코드를 주기적으로 갱신하면 됨
    • 그냥 포기하고 Tailscale을 쓰면 어둠의 편에서 삶이 훨씬 편해짐
    • VPN 안에서는 원하는 주소를 하드코딩해도 되는데 왜 동적 DNS 레코드가 필요한지 모르겠음
      네트워크를 직접 통제하고 있으니, 내 VPN에서는 IP 주소를 바꿔야 할 필요가 없었음
  • 이 글은 WireGuard에서 NAT를 써야 하는 것처럼 보이게 하지만 실제로는 전혀 그렇지 않음
    목적지 호스트가 WireGuard 서브넷으로 가는 게이트웨이로 WireGuard 서버를 쓰도록 알고 있으면 일반적인 서브넷 라우팅이 잘 동작함
    보통 기본 라우터에 정적 경로 하나만 설정해도 충분하고, 물론 공용 DNS 사용 시도를 로컬 DNS로 돌리는 것처럼 NAT가 유용한 경우도 있음

  • 글에서 IPv6를 다뤘으니, WireGuard 앱이 아직 해결하지 못한 큰 함정을 짚고 싶음
    미국 T-Mobile 같은 464XLAT 환경에서는 IPv6 주소만 있고, IPv4 엔드포인트만 있는 서버와 통신할 때 T-Mobile 소유 프록시를 거침
    CGNAT처럼 기기와 NAT 게이트웨이 사이에 IPv4 트래픽이 있는 게 아니라, 기기에서 나가는 트래픽은 항상 IPv6임
    iOS 기본 WireGuard 앱은 A와 AAAA를 모두 제공하는 DNS 이름에 연결하도록 설정하면 기본적으로 A 레코드를 골라서, T-Mobile에서는 이 프록시를 타게 되고 프록시 연결이 시간이 지나며 끊겨 푸시 알림 같은 것이 멈춤
    해결책은 IPv6 주소만 넣은 별도 설정을 만들어 그쪽에 연결하는 것임
    중간 프록시가 사라지고, KeepAlive를 꺼도 푸시 알림이 계속 동작함

  • WireGuard를 IPv6와 함께 쓰고 있는데, 일반 이더넷 서브넷처럼 기기가 자기 주소를 고르고 바꿀 수 있게 하는 IPv6 프리픽스 위임만은 끝내 성공하지 못했음
    요청을 보낸 휴대폰이나 기기가 서브넷 안에서 드러나지 않게 해주는 일반적인 무작위화가 마음에 듦

    • 프리픽스 위임은 모르겠지만, 네이티브 클라이언트가 WireGuard 위의 라우터 광고(RA) 는 잘 받아들였음
      지금은 /64 하나만 있어서 실제로 이 메커니즘을 쓰긴 어렵지만, radvd에 아래 설정을 줘서 ULA를 구성해봤음
      fdf4:a694:0e43::/48에 해당하는 범위를 모든 인터페이스에서 써서 큰 노력 없이 ULA를 라우팅 가능하게 둠
      인터페이스에 전체 /64를 광고할 주소 공간만 있다면 일반적인 IPv6 SLAAC 구성을 못 할 이유는 없어 보임
    • WireGuard의 암호키 라우팅 때문에 닭과 달걀 같은 문제가 생김
      다소 아쉽게 이름 붙은 allowed-ips 매개변수가 패킷을 어느 피어로 라우팅할지 결정함
      중앙 VPN 서버 하나에 피어 셋이 연결된 상황을 생각하면, 서버 관점에서는 각 피어에 같은 /64 네트워크를 allowed-ips로 설정해야 해서 충돌이 생김
      allowed-ips를 동적으로 설정하는 프로젝트가 있긴 하지만 아쉽게도 더 이상 활발하지 않음: https://github.com/WireGuard/wg-dynamic/blob/master/docs/ide...
    • 명세 자체가 그걸 지원하는지는 모르겠지만 좋은 기능 요청으로 보임
      새 IPv6 주소가 연결될 때마다 WG 설정을 갱신해야 하므로, 설정 갱신을 밀어 넣고 서비스를 재시작할 수 있는 클라이언트 같은 계층을 거쳐야 할 듯함
      불가능하진 않지만 유지해야 할 복잡성이 한 겹 더 생김
  • RBAC가 없는 건 아쉽지만 이해는 됨
    WireGuard는 OpenVPN보다 훨씬 빠르고, 우리는 사이트 간 연결에는 WireGuard를 쓰지만 직원과 계약자용으로는 RBAC 때문에 OpenVPN이 필요함
    WireGuard에 RBAC를 억지로 끼워 넣으려는 글들은 결국 다들 “이렇게 하지 말라”로 끝났음

    • WireGuard 설계의 핵심은 이런 상위 계층 관심사에 불가지론적이라는 데 있음
      빠르고 선택적으로 커널에 상주하는 보안 전송 계층이고, 그 위에 원하는 것을 만들 수 있음
      WireGuard가 RBAC를 다루는 것도 아니고 “RBAC를 하지 말라”는 입장을 가진 것도 아님
    • RBAC가 있는 아주 좋은 WireGuard 구현이 있고, 이름은 Tailscale
    • 이 용도로는 Defguard가 마음에 듦: https://defguard.net/
    • 커널 WireGuard는 DCO 없는 OpenVPN보다 빠를 수 있고 실제로 자주 그렇지만, DCO가 있는 OpenVPN은 커널 WireGuard보다 상당히 빠른 경우가 많음
      DCO는 Linux, FreeBSD, Windows에서 사용 가능함
  • 좋은 글이고 아이디어도 멋짐
    내 인프라는 IPv6에 의존하고 있고, 집 네트워크와 서버가 모두 IPv6로 공개 라우팅 가능함
    원글의 IPv6 설정과 비슷한 방식으로 스마트폰에도 IPv6 연결성을 줘서 내 인프라에 접근하게 함
    다만 WireGuard를 내부적으로 써서 공개 서버를 노출하면 무엇을 얻는지 분명하지 않음
    그냥 서버에 IPv6 주소를 3계층에서 할당하고 평소처럼 라우팅하면 안 되는 이유가 뭘까
    대부분의 인프라가 공개 라우팅 가능한 IPv6를 갖고 있으니, 그 주소 계층은 유지하면서 WireGuard의 현대적 암호화와 상태 없는 설계만 누리고 싶음
    결국 원하는 건 주소 지정을 뺀 WireGuard, 또는 상태 없는 IPsec 전송 모드 같은 것임

  • 실제 도메인 이름으로 웹에서 접근할 수도 있고 Tailscale로만 접근할 수도 있는 개인 서버를 만들고 있음
    Caddy로 서브도메인을 서비스에 매핑하는 웹 부분은 됐지만, 문제는 Tailscale Magic DNS가 서브도메인을 지원하지 않는다는 것
    blah.blah.ts.net/svc1 같은 경로에 서비스를 올리고 Caddy에서 경로를 잘라낼 수도 있지만, 서비스별로 링크 깨짐이나 웹소켓 깨짐 같은 문제를 디버깅해야 해서 깔끔하지 않음
    그래서 서브도메인이 유일하게 깔끔한 해법처럼 보임
    원하는 서브도메인을 Tailscale IP로 해석하는 사설 DNS 서버를 만드는 게 이 제한을 우회하는 최선인 듯해서 Pi-hole 설정을 시도 중인데, 이게 WireGuard의 제한인지 궁금함
    이런 네트워크는 보통 어떻게 구성하는 걸까

    • WireGuard와 Tailscale은 같은 것이 아니고, Tailscale Magic DNS는 WireGuard와 전혀 관계가 없음
      “그냥 Tailscale 쓰라”는 조언이 나쁜 이유를 잘 보여주는 예임
      훌륭한 기능도 있지만, 그 기능들이 필요 없다면 긴밀하게 통합된 네트워크 스택에 불필요하게 묶이고, 정해진 길에서 벗어날 때마다 방해가 됨
      정말 개인용 애플리케이션이라면 Tailscale을 버리고 그냥 WireGuard를 쓰는 편을 권함
      OpenWRT나 pfSense 같은 웬만한 라우터 소프트웨어는 WireGuard를 가상 네트워크 인터페이스로 실행하고 로컬 DNS 서버에서 정적 레코드, 위임 등을 원하는 대로 설정할 수 있음
      인증서는 직접 처리해야 하지만, 로컬 DNS를 Tailscale과 잘 맞추려 해도 어차피 마찬가지임
    • 결국 그렇게 했고 몇 달 전에 블로그 글도 썼음: https://garrido.io/notes/tailscale-nextdns-custom-domains/
      요지는 사설 DNS를 쓰고 서비스를 사설 네트워크에만 노출하는 것임
      구현 세부는 달라질 수 있고, Tailscale을 쓰든 순수 WireGuard를 쓰든, 리버스 프록시와 DNS 서버는 아무거나 가능함
      내 경우에는 Tailscale, NextDNS, Caddy를 씀
    • 접근할 서비스가 많지 않다면 /etc/hostsTailscale IP 주소를 하드코딩할 수 있음
      내 개인 /etc/hosts에는 서비스 10개가 모두 하드코딩돼 있고, Tailscale에서 머신의 내부 IP는 정적이라 별도 DNS 해석기를 세우는 것보다 훨씬 싸고 쉽음
      물론 수백 개나 수천 개 서비스를 다뤄야 한다면 이 방식은 맞지 않음
    • 도메인이 있다면 Tailscale IP에 대한 DNS 레코드를 그냥 만들면 됨
      또는 Tailscale 네트워크 안에서 Pi-hole이나 AdGuard 같은 자체 DNS 서버를 실행하고 원하는 DNS 레코드를 정의할 수 있음
  • 모든 트래픽을 터널링하되, 특정 IP만 제외하는 쉬운 내장 방식이 왜 없는지 늘 조금 의아했음
    AllowedIPs = 0.0.0.0/0, ::/0에서 일부 IP만 제외하고 싶은데, 결국 그 IP들을 제외한 나머지 전체를 포함하는 거대한 CIDR 목록을 프로그램으로 만들어야 함

    • 그 기능은 유용할 것 같음
      이유는 AllowedIPs의 모든 항목이 그대로 라우팅 테이블에 쓰이고, 커널의 라우팅 로직이나 대부분의 라우터에는 불일치 조건이 없기 때문일 가능성이 큼
      대신 제외할 CIDR이 어디로 가야 하는지 명시적 경로를 추가하면 됨
      그 경로가 더 구체적이므로 WireGuard 터널을 가리키는 0.0.0.0/0 같은 경로보다 패킷 매칭에 우선 사용됨
    • 우회용 계산기: https://www.procustodibus.com/blog/2021/03/wireguard-allowed...
    • https://github.com/tailscale/tailscale/issues/11717#issuecom... 참고
    • 설정에 피어를 하나 더 추가하고 제외할 IP들을 거기에 넣은 뒤, 절대 연결하지 않게 두면 가능한지 궁금함
    • 사전 라우팅 방화벽 규칙으로 할 수 없을까
      직접 해본 건 아니지만 가능해 보임
  • WireGuard에서 제대로 못 해낸 것 중 하나가 마스커레이딩 없는 포트 포워딩
    원본 IP를 그대로 유지해야 하는데, AllowedIPs0.0.0.0/0을 추가하지 않으면 WireGuard 피어가 패킷을 버림
    그렇다고 AllowedIPs0.0.0.0/0을 넣으면 경로가 추가되어 내 애플리케이션의 응답이 원본으로 돌아가지 못함
    결국 포기했고, 이걸 제대로 동작시키려면 nft나 firewalld 규칙에 무엇을 넣어야 하는지 아는 사람이 없었음

    • wg-quick을 쓰고 있다면 Table = off로 시스템 라우팅 테이블에 경로가 자동 추가되는 것을 꺼야 함
      그다음 경로를 수동으로 추가하면 됨
    • 공용 인터넷 클라이언트가 내 VPS에 연결하면, WG가 443 같은 포트 트래픽을 집에 있는 WG 클라이언트로 라우팅하고, 거기서 Apache 리버스 프록시를 거쳐 kube 클러스터 안의 Spring Boot 앱으로 감
      로그에는 들어온 공용 인터넷 클라이언트의 IP가 표시됨
      응답도 그대로 인터넷 클라이언트까지 되돌아감
      이게 논의 중인 목표를 달성한 게 아닌지 궁금하고, 서버와 클라이언트 설정에서 민감한 부분을 지운 버전을 올릴 수 있음