WireGuard 설정 및 컨셉 정리

WireGuard 설정 및 컨셉 정리

네트워크 A에서 네트워크 B에 있는 리눅스 서버 접속을 위해 WireGuard VPN을 사용하고 있다. WireGuard VPN을 설정하며 동작 원리를 이해하지 못해 많은 시행 착오가 있었기에 설정 방법을 정리하고, 또 그동안 배웠던 WireGuard 컨셉에 관한 내용도 정리한다.

Linuxserver WireGuard vs wg-easy

예전에 WireGuard를 처음 사용했을 때는 wg-easy를 사용했다. 하지만 wg-easy는 Linuxserver 버전에 비해 관리가 잘 안되는 느낌이고, 결정적으로 Oracle Cloud에서 동작이 안 됐다. 많은 시도 후 알아낸 것이 WireGuard에서 사용하는 network interface에서 eth0 같은 default network interface로 패킷 라우팅해주는 iptable rule을 설정해줘야 하는데, 이것이 자동으로 설정이 안 됐다. 수동으로 넣어주면 되긴 하겠지만, 이미 wg-easy 설정에 많은 시간을 썼고, 추가적으로 iptable rule 설정에 시간을 쓰고 싶지 않았다.

반면에 Linuxserver 버전은 알아서 필요한 iptable rule을 잘 넣어줬고, 매우 쉽게 VPN 연결되었다. 또한 Linuxserver 도커는 자주 업데이트되고 있었기에 잘 관리되고 있다는 느낌을 받았다.

서버 설정

Linuxserver 버전을 사용해보니 설정이 오히려 wg-easy보다 더 쉬웠다. Linuxserver 버전 도커에서는 WireGuard를 "서버"모드 또는 "클라이언트" 모드로 설정할 수 있게 되어있다. 여기서는 서버 모드로 설정한다.

다음과 같이 docker-compose.yml 작성한다. Peers는 나중에 필요시 추가할 수 있다. 추가할 시 기존에 생성되었던 Peer의 Public/Private key는 그대로 유지된다. 방법은 PEERS를 변경하고 docker를 재시작한다.

version: "3"
services:
  wireguard:
    image: linuxserver/wireguard
    container_name: wireguard
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    environment:
      - PUID=1001
      - PGID=1001
      - TZ=Asis/Seoul
      - SERVERPORT=51820 #optional
      - PEERS=10 #optional
      - PEERDNS=auto #optional
    volumes:
      - ./config:/config
      - /lib/modules:/lib/modules
    ports:
      - 51820:51820/udp
    sysctls:
      - net.ipv4.conf.all.src_valid_mark=1
    restart: unless-stopped

도커를 시작한다.

$ mkdir -p config
$ docker compose up -d

그러면 서버의 Interface와 Peer 들의 정보가 자동 생성된다.

$ more config/wg0.conf
[Interface]
Address = 10.13.13.1
ListenPort = 51820
PrivateKey = 8CgvSv...
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth+ -j MASQUE
RADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth+ -j MASQ
UERADE

[Peer]
# peer1
PublicKey = TTh8yR...
PresharedKey = 93xENM...
AllowedIPs = 10.13.13.2/32

[Peer]
# peer2
PublicKey = lhK2Js...
PresharedKey = qn10Co...
AllowedIPs = 10.13.13.3/32

...

Linuxserver 버전에서는 친절하게 클라이언트 설정 시 필요한 configuration도 자동으로 생성해준다. 또한 핸드폰 클라이언트에서 사용할 수 있는 QR 코드도 생성해준다.

$ more config/peer1/peer1.conf
[Interface]
Address = 10.13.13.2
PrivateKey = SD3Xv/...
ListenPort = 51820
DNS = 10.13.13.1

[Peer]
PublicKey = xmSlHl...
PresharedKey = 93xENM...
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0

클라이언트 설정

WireGuard 클라이언트를 설치한다. Add Tunnel->Add empty tunnel을 선택한 후 위의 peer1.conf를 선택한다. Linuxserver 버전에서는 클라이언트 설정을 다 해주니 매우 편하다.

WireGuard 동작 컨셉

사실 이 글을 작성한 이유가 WireGuard 동작 원리를 기록해 놓기 위함이다. (설정 방법이야 쉽게 찾아볼 수 있다.)

기존 네트워크 A에서 네트워크 B를 WireGuard로 연결해서 사용하고 있었는데, 네트워크 C를 추가하면서, 네트워크 B에서 네트워크 C로 VPN 연결도 필요하게 되었다. 이 경우 B에 있는 리눅스 머신은 WireGuard의 서버이자 동시에 클라이언트로서 동작해야 한다. 이 경우 설정 방법을 찾아보기 어려웠기에 지금까지 찾아보았던 동작 원리를 정리해본다.

WireGuard는 보통 VPN 구현에서 사용하는 서버와 클라이언트 간의 one-directional 통신이 아닌 Peer와 Peer 간의 bidirectional 통신 구조로 되어있다. 통신을 위해 Peer 등록을 해야 하고, 등록은 서로의 Public 키를 exchange 하는 방식으로 한다. 즉 Peer A와 Peer B가 서로 연결하고 싶을 경우 Peer A는 Peer B의 Public 키를, Peer B는 Peer A의 Public 키를 가지고 있게 된다. 만약 Peer A가 Peer C와 통신을 하고 싶을 경우, Peer A는 Peer C의 Public 키도 가지고 된다. 그리고 Peer A와 Peer B는 서로 Public 키를 사용하여 통신 가능한지를 확인한다. 당연히 서로의 Public 키가 자신이 가지고 있는 Private 키와 매칭이 될 경우에만 1:1 통신이 가능하다.

Peer는 Interface를 가지고 있다. Interface에서는 패킷을 암호화한다. Peer에서 생성된 패킷은 Interface에서 암호화되고 암호화된 패킷은 Peer의 default network interface를 통해 나가게 된다. 그리고 패킷은 다른 Peer에 도착하여 암호화 반대의 과정을 거쳐 원본 상태로 복구된다.

여기서 iptable의 패킷의 forwarding rule이 필요하다. 특정 패킷은 Interface를 통해 default network interface를 통해 나갈 수 있게 firewall 세팅이 필요한데, wg-easy는 이 부분이 잘 안되었다.

다음은 자동 생성된 configuration 파일에서 interface와 peer의 관계를 알아본다.

각 Peer에서는 Public/Private 키를 생성하고 Public 키를 다른 Peer에게 전달해야 한다. 그리고 서로 통신에 필요한 IP와 Port를 교환해야 한다. 원래는 이 부분을 직접 설정해야 하고 이 과정에서 실수하기 매우 쉽다. 그런데 Linuxserver에서는 이 부분을 자동으로 생성해 주었다.

여기서 AllowedIPs가 중요하다. AllowedIPs는 Peer의 interface를 통과할 수 있는 IP 주소를 의미한다. 예를 들어 Peer A (Client)의 AllowedIPs가 0.0.0.0/0일 경우, Peer A에서 발생하는 모든 IP 주소는 Peer A의 Interface를 통해 나갈 수 있다. 즉 모든 패킷은 VPN을 통해 나가게 된다. VPN의 용도가 Peer A의 트래픽이 마치 Peer B에서 발생하는 트래픽인 것 보이게 하려면 AllowedIPs를 이렇게 설정하면 된다. 물론 당연히 Peer B에 있는 리눅스 서버에도 접속 가능하다.

하지만 이렇게 설정할 경우 Peer A에서 문제가 생기는데, 192.168.1.1/24 같은 로컬 IP에 접속할 수 없게 된다. 당연히 이 문제의 원인은 Peer A의 로컬 IP도 Interface를 통해 Peer B로 흘러가고, Peer B에서 192.168.1.1/24를 찾기 때문이다. 즉 VPN이 연결된 상태에서는 Peer A의 로컬 네트워크를 사용하지 못한다. 이를 해결하기 위해 AllowedIPs를 사용한다.

AllowedIPs에는 Peer A의 Interface를 통해 나갈 수 있는 IP를 적어준다. 만약 리눅스 서버의 주소가 10.0.0.2일 경우, AllowedIPs에 10.0.0.1/24 같이 리눅스 서버의 IP 또는 IP의 range를 적어준다. 이렇게 설정할 경우 Peer A에서는 192.168.1.1/24 같은 주소는 Interface를 통과하지 못하고 default network interface를 통해 나가게 된다. 10.0.0.2 경우 AllowedIPs에 포함되기에 Interface를 통해 Peer B로 전달된다.

그럼, Peer B에서 Peer A에 연결할 수 있을까? Linuxserver에서 자동 생성된 서버의 configuration을 보면 알 수 있다. 서버의 Peer는 10.13.13.2/32만 적혀있다. 즉 Peer A의 클라이언트, 예를 들어 192.168.1.2 IP를 가진 윈도우 클라이언트의 IP가 없다. 그래서 연결하지 못한다.

Peer B에서 Peer A로 연결하고 싶으면 어떻게 해야 할까? 여기서 Linuxserver에서 작성한 도커 문서를 살펴보면 도커는 "서버모드" 또는 "클라이언트"모드로 설정해서 동작한다는 것을 알 수 있고, 이제서야 이 의미를 이해할 수 있다. 자동으로 생성되는 configuration으로는 이렇게 설정할 수 없다.

Peer A와 B 간 서로 통신할 수 있는 설정을 Site-to-Site VPN이라고 하고 Linuxserver 도커에서는 비공식적인 방법으로 지원한다. 이를 위해 Configuration 파일에 AllowedIPs를 수정해줘야 한다. 방법은 도커 설정 페이지에 나와 있다.

이제 원래 하려고 했던 서버와 클라이언트를 동시에 수행하려는 문제를 해결할 수 있게 되었다. 가장 쉬운 방법으로는 서버와 클라이언트 도커를 두 개 띄워주면 된다. 이때 Interface의 포트는 서로 다르게 설정해야 한다.

PS. 홈 네트워크에서 OPNsense를 사용하고 있고, 여기에 WireGuard를 설정하여 외부에서 홈 네트워크를 연결할 수 있게 하였다. 이제 필요한 것이 OPNsense를 WireGuard 클라이언트로 설정하여 홈 네트워크에 있는 기기가 외부 네트워크에 있는 기기에 연결할 수 있게 하려고 한다. 근데 이게 잘 안돼서 우선 WireGuard에 대해 정리하다 보니 긴 글이 되었다. 아직 하려고 한 건 시작도 못 했는데...