SSD Nodes Learn 🎉 VPS $5.50/월부터
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-08-21

CGNAT 환경에서 VPS 리버스 터널로 포트 포워딩 구현하기

CGNAT로 인해 공인 IP 할당이 불가능한 환경에서 frp를 사용하여 외부 접속을 해결하는 방법을 안내합니다. 저렴한 VPS를 활용해 가정용 서버로 트래픽을 전달하고, HTTPS 인증서를 적용하여 안정적인 서비스를 운영하는 구체적인 설정 과정을 다룹니다.

CGNAT 환경에서 포트 포워딩이 작동하지 않는 이유

CGNAT(Carrier-Grade Network Address Translation) 환경에서는 라우터의 WAN 주소를 다른 가입자와 공유합니다. 따라서 사용자에게 할당된 공인 IP가 없으며, 포트 포워딩을 설정할 대상도 존재하지 않습니다. 리버스 터널을 사용하면 이 문제를 해결할 수 있습니다. 공인 IP를 가진 저렴한 VPS를 준비하고, 가정용 서버에서 VPS로 연결을 시도하면 외부 요청이 이미 열려 있는 연결을 타고 가정용 서버로 전달됩니다. 기존 하드웨어를 그대로 유지하면서, ISP가 제공하지 않는 라우팅 가능한 주소라는 자원만 VPS를 통해 임대하는 방식입니다.

아래의 모든 명령어는 실행할 장비가 명시되어 있습니다. 이 구성을 위해서는 공인 IP를 가진 VPS와 서비스를 호스팅할 가정용 서버, 총 2대의 장비가 필요합니다.

CGNAT 환경인지 확인하는 방법

라우터의 관리 페이지에 접속하여 표시되는 WAN 주소를 확인하십시오. 그런 다음 외부 인터넷 서비스를 통해 현재 외부에서 인식하는 공인 IP 주소를 확인하십시오.

# on the home box
curl -4 -s https://ifconfig.me; echo

두 주소가 일치한다면 공인 IP를 사용 중이므로 이 가이드를 따를 필요가 없습니다. 포트 포워딩을 설정하고 작업을 종료하십시오. 라우터의 WAN 주소가 100.64.0.0/10 대역에 속한다면 CGNAT 환경입니다. 해당 대역은 RFC 6598에서 정의한 공유 주소 공간으로, 정확히 이러한 용도로 예약되어 있습니다. 일부 ISP는 WAN 측에 10.0.0.0/8 대역을 할당하기도 하는데, 이는 명칭만 다를 뿐 동일한 상황입니다.

무언가를 대여하기 전에 한 가지를 먼저 확인하십시오. 많은 CGNAT 환경의 ISP가 실제 IPv6 프리픽스를 할당합니다. 만약 가정용 장비에 글로벌 IPv6 주소가 할당되어 있다면, 해당 주소의 방화벽을 개방하여 터널링 없이 서비스를 운영할 수 있습니다. 다만 방문자가 IPv4 전용 네트워크를 사용하는 경우에는 접속이 불가능하므로, 대부분의 사용자가 결국 이 가이드를 찾게 됩니다.

VPS 리버스 터널의 내부에서 외부로의 연결 방식

CGNAT와 일반적인 가정용 라우터는 외부에서 들어오는 원치 않는 연결을 차단합니다. 기업용 방화벽도 마찬가지입니다. 하지만 이들 모두 아웃바운드 연결은 차단하지 않습니다. 모든 브라우저와 업데이트 클라이언트가 상시 수행하는 작업이기 때문입니다. 아웃바운드 TCP 연결을 감지한 NAT 장치는 해당 연결에 대한 매핑을 생성하고, 그 연결을 통해 들어오는 응답 트래픽을 허용합니다. 외부에서는 가정용 장비로 연결을 시작할 수 없습니다. 따라서 가정용 장비가 먼저 연결을 시작하고, 터널은 해당 연결을 통해 트래픽을 반대 방향으로 전달합니다.

이것이 전체 메커니즘의 전부입니다. 가정용 장비는 특정 포트를 통해 VPS로 연결을 시도하고 해당 연결을 유지합니다. VPS는 외부의 요청을 수락하여 이미 생성된 연결을 통해 내부로 전달합니다. 외부에서는 가정용 IP 주소로 직접 접근을 시도하지 않으므로, 별도의 설정이 필요하지 않습니다.

이 방식에는 유용한 두 가지 결과가 따릅니다. 첫째, DNS 레코드는 가정집이 아닌 VPS를 가리킵니다. 둘째, 외부에서 확인되는 공인 IP 주소는 가정용 회선이 아닌 임대 서버의 주소이므로, IP 조회를 통해 서버의 위치를 추적하더라도 가정용 회선 정보는 노출되지 않습니다.

구축을 위한 세 가지 방법

  1. ssh -R: 양쪽 끝단에 이미 설치되어 있으며, 단일 서비스나 일시적인 데모에 적합합니다. 대시보드가 없으며 제대로 된 재연결 로직도 제공하지 않습니다.
  2. frp: 소형 Go 서버(frps)와 그에 대응하는 클라이언트(frpc)입니다. 하나의 호스트 이름 뒤에 여러 서비스를 두는 영구적인 설정에 적합합니다. 이 가이드의 대부분은 이 내용을 다룹니다.
  3. 메시 VPN: Tailscale 또는 직접 운영하는 WireGuard 서버입니다. 무언가를 공용 인터넷에 공개하는 것이 아니라, 본인의 기기들이 서로 비공개로 통신해야 할 때 적합합니다.

본인이 제어하는 기기에서 비공개로 접근하는 것이 목적이라면 메시 VPN을 선택하십시오. Tailscale Serve and Funnel은 tailnet 외부로 서비스를 공개하는 방법을 다루며, a self-hosted WireGuard VPN on the same VPS는 제3자의 조정 서버 없이 동일한 형태의 네트워크를 구성하는 방법을 설명합니다. 해당 문서 중 하나를 읽고 이 페이지의 나머지 내용은 건너뛰어도 됩니다. 아래의 모든 내용은 누구나 접속할 수 있는 공개 HTTPS 호스트 이름을 원하는 경우를 가정합니다.

요약: 단일 서비스를 위한 ssh -R

홈 서버에서 애플리케이션을 127.0.0.1:3000 포트로 실행 중이고, 이미 VPS에 SSH로 접속할 수 있다고 가정합니다.

# on the home box
ssh -N \
  -o ExitOnForwardFailure=yes \
  -o ServerAliveInterval=30 \
  -o ServerAliveCountMax=3 \
  -R 127.0.0.1:8080:127.0.0.1:3000 \
  tunnel@vps.example.com

-R 127.0.0.1:8080:127.0.0.1:3000 옵션은 VPS의 sshd가 자신의 127.0.0.1:8080 포트에서 대기하다가, 들어오는 모든 트래픽을 홈 서버의 127.0.0.1:3000 포트로 전달하도록 지시합니다. -N 옵션은 셸을 실행하지 않도록 합니다. 두 개의 ServerAlive 옵션을 사용하면 연결이 끊겼을 때 무한정 대기하지 않고 약 90초 이내에 이를 감지합니다.

이제 가장 혼란스러운 부분입니다. 이 리스너는 루프백 인터페이스에서 동작하므로, 외부에서 curl http://vps.example.com:8080으로 접속하면 실패합니다. sshd는 기본적으로 GatewayPorts no 설정이 적용되어 있어, 원격 포트 포워딩이 루프백 인터페이스에만 바인딩됩니다. 이를 해결하기 위해 GatewayPorts yes 설정을 변경하지 마십시오. 포워딩은 루프백에 그대로 두고, 아래 설명할 frp 설정과 마찬가지로 앞에 nginx를 배치하십시오. 이렇게 하면 공개 포트는 인증서가 적용된 443 포트가 되며, 터널 포트는 인터넷에 직접 노출되지 않습니다. 현재 어떤 인터페이스에서 어떤 포트가 대기 중인지 확실하지 않다면, Linux의 포트 및 리스너 확인 방법을 10분 정도 읽어보는 것을 권장합니다.

VPS에서 해당 포트를 이미 다른 프로세스가 점유하고 있다면 SSH는 다음과 같은 메시지를 출력하며, ExitOnForwardFailure=yes 옵션을 사용하면 터널이 정상적으로 작동하지 않을 때 연결을 즉시 종료합니다.

Warning: remote port forwarding failed for listen port 8080

일반적인 원인은 이전 세션이 sshd가 감지하지 못한 채 비정상 종료된 경우입니다. VPS의 /etc/ssh/sshd_config 파일에 ClientAliveInterval 30ClientAliveCountMax 3를 설정하여, 끊긴 세션을 정리하고 포트를 해제하도록 하십시오. 전체 명령을 Restart=always과 전용 키를 사용하는 systemd 유닛으로 감싸거나, autossh을 사용하십시오. 서비스가 두 개 이상이라면 여기서 멈추고 frp를 사용하십시오.

VPS에 frp 설치 및 특정 버전 고정

frp는 정적 Go 바이너리로 제공되며 Ubuntu나 Debian 저장소에는 포함되어 있지 않으므로, 릴리스를 직접 다운로드하여 검증해야 합니다. 버전을 고정하십시오. 설정 형식은 v0.52.0에서 변경되었고 이후에도 옵션 이름이 계속 바뀌었으므로, 오래된 튜토리얼을 따라 하면 현재 바이너리가 인식하지 못하는 키를 사용하게 될 수 있습니다. 이 가이드는 2026년 8월 14일에 배포된 v0.71.0을 사용합니다.

# on the VPS
FRP_VERSION=0.71.0
ARCH=amd64   # use arm64 if `uname -m` prints aarch64
cd /tmp
curl -fsSLO "https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_linux_${ARCH}.tar.gz"
curl -fsSLO "https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_sha256_checksums.txt"
sha256sum --check --ignore-missing frp_sha256_checksums.txt

sha256sum는 정확히 한 줄을 출력해야 합니다.

frp_0.71.0_linux_amd64.tar.gz: OK

--ignore-missing 플래그가 필요한 이유는 체크섬 파일이 18개의 모든 릴리스 에셋을 포함하고 있는데, 사용자는 그중 하나만 다운로드했기 때문입니다. 이 플래그가 없으면 sha256sum은 나머지 17개 파일이 누락되었다고 보고하며 0이 아닌 종료 코드를 반환합니다. 이는 아무런 문제가 없음에도 검증에 실패한 것처럼 보이게 만듭니다.

# on the VPS
tar xzf "frp_${FRP_VERSION}_linux_${ARCH}.tar.gz"
sudo install -m 755 "frp_${FRP_VERSION}_linux_${ARCH}/frps" /usr/local/bin/frps
sudo useradd --system --no-create-home --shell /usr/sbin/nologin frp
sudo install -d -m 750 -o root -g frp /etc/frp
frps --version

frps --version0.71.0을 출력합니다. frps만 VPS에 설치하십시오. frpc는 가정용 기기에 설치합니다. 모든 바이너리를 어디에나 설치하면 실수로 가정용 기기에서 터널 서버를 실행하게 될 수 있습니다.

VPS 설정: 토큰, 강제 TLS, 루프백 리스너

먼저 토큰을 생성하십시오. 이 토큰은 VPS의 포트를 스캔하는 외부 공격자로부터 터널을 보호하는 유일한 수단입니다.

# on the VPS
openssl rand -base64 32

해당 값을 /etc/frp/frps.toml에 기록하십시오:

bindAddr = "0.0.0.0"
bindPort = 7000

# Every listener frp creates for a proxy, including the HTTP vhost, stays on loopback.
proxyBindAddr = "127.0.0.1"
vhostHTTPPort = 8080

auth.method = "token"
auth.token = "PASTE_THE_OPENSSL_OUTPUT_HERE"

transport.tls.force = true

webServer.addr = "127.0.0.1"
webServer.port = 7500
webServer.user = "admin"
webServer.password = "PASTE_A_SECOND_SECRET_HERE"

log.level = "info"

다음 네 줄은 보안을 담당하므로 하나씩 확인해야 합니다.

auth.token은 클라이언트의 auth.token과 일치해야 합니다. 이 설정이 없으면 frps는 포트 7000을 찾아내는 모든 클라이언트를 수락하며, 해당 클라이언트는 사용자의 VPS와 인증서를 통해 무엇이든 게시할 수 있게 됩니다.

transport.tls.force = true는 TLS(전송 계층 보안)가 아닌 모든 제어 연결을 거부합니다. 클라이언트는 v0.50.0 버전부터 기본적으로 TLS를 사용하므로 실제 운영상 손해는 없으며, 구형 클라이언트나 직접 만든 클라이언트가 암호화되지 않은 상태로 연결되는 경우를 차단합니다.

proxyBindAddr = "127.0.0.1"은 대부분의 가이드에서 누락하는 설정이며, 이 설정을 통해 본 구성은 안전하게 유지됩니다. 이 옵션은 HTTP vhost와 클라이언트가 요청하는 모든 remotePort을 포함하여, frp가 프록시를 대신해 여는 모든 리스너를 루프백 인터페이스로 이동시킵니다. 따라서 인터넷에서는 해당 리스너에 직접 접근할 수 없습니다. 유일한 공개 통로는 사용자가 직접 설정하고 제어하는 443 포트의 nginx뿐입니다.

webServer.addr = "127.0.0.1"는 대시보드가 공개 인터페이스에 노출되지 않도록 합니다. 대시보드는 사용자의 사설 서비스와 트래픽 정보를 모두 담고 있으며 HTTP 기본 인증 비밀번호 하나로만 보호되므로, 0.0.0.0에 노출해서는 안 됩니다.

토큰을 다른 사용자가 읽을 수 없도록 소유권을 설정한 뒤, 서비스를 시작하기 전에 구문 오류를 확인하십시오:

# on the VPS
sudo chown root:frp /etc/frp/frps.toml
sudo chmod 640 /etc/frp/frps.toml
sudo -u frp frps verify -c /etc/frp/frps.toml

올바른 파일이라면 다음과 같이 출력됩니다:

frps: the configuration file /etc/frp/frps.toml syntax is ok

시간을 절약할 수 있는 형식 관련 참고 사항입니다. frp는 파일 확장자를 통해 파서를 선택하며, .toml, .yaml, .yml, .json 형식을 인식합니다. 구형 .ini 파일도 레거시 변환 경로를 통해 로드되지만, INI 형식은 더 이상 권장되지 않으며 새로운 옵션은 TOML 형식으로만 문서화됩니다. 만약 튜토리얼에서 [common] 섹션과 server_addr = x.x.x.x을 보여준다면, 이는 v0.52.0 이전 버전의 자료이므로 키 이름이 현재 설치된 바이너리와 일치하지 않을 것입니다.

권한이 없는 서비스로 frps 실행하기

bindPort은 7000번이고 vhostHTTPPort는 8080번입니다. 두 포트 모두 1024번보다 크므로 frps는 root 권한이 필요 없으며 CAP_NET_BIND_SERVICE도 필요하지 않습니다. 이것이 vhost를 80번 포트에 두지 않고 대신 nginx가 이를 처리하도록 하는 이유입니다.

/etc/systemd/system/frps.service를 작성합니다:

[Unit]
Description=frp reverse tunnel server
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=frp
Group=frp
ExecStart=/usr/local/bin/frps -c /etc/frp/frps.toml
Restart=on-failure
RestartSec=5s
LimitNOFILE=65535
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true

[Install]
WantedBy=multi-user.target
# on the VPS
sudo systemctl daemon-reload
sudo systemctl enable --now frps
sudo journalctl -u frps -n 20 --no-pager

로그에는 두 리스너가 모두 표시되어야 하며, 포트보다 주소가 더 중요합니다:

frps tcp listen on 0.0.0.0:7000
http service listen on 127.0.0.1:8080

ProtectSystem=strict는 이 서비스에 대해 전체 파일 시스템을 읽기 전용으로 만듭니다. frps는 로그를 기본적으로 표준 출력으로 보내고 journald가 이를 캡처하므로 이러한 설정을 허용합니다. 만약 log.to을 파일 경로로 설정하면, 일치하는 ReadWritePaths= 라인을 추가하기 전까지 서비스가 로그를 기록하지 못해 실패하게 되므로 기본값을 그대로 두십시오.

방화벽: 범위가 아닌 단일 포트 개방

# on the VPS
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 7000/tcp
sudo ufw enable
sudo ufw status numbered

총 4개의 규칙이 필요하며, 그중 하나는 인증서 갱신만을 위한 것입니다. 22번은 SSH용입니다. 80번은 443번으로 리다이렉트하며 ACME(자동 인증서 관리 환경) 챌린지에 응답합니다. 443번은 터널링된 모든 애플리케이션을 서비스합니다. 7000번은 frp 제어 포트이며, 클라이언트가 접근해야 하는 유일한 포트입니다.

sudo ufw allow 20000:30000/tcp와 같이 포트 범위를 개방하라고 안내하는 가이드는 각 서비스가 고유한 공개 TCP 포트를 점유하는 다른 설계 방식을 설명하는 것입니다. 이 구성에서는 모든 트래픽이 443번으로 들어와 frp가 호스트 이름에 따라 라우팅하므로 그럴 필요가 없습니다. 만약 나중에 진정으로 공개된 TCP 포트가 하나 필요하다면, proxyBindAddr0.0.0.0으로 되돌리고 클라이언트가 지정한 포트만 점유할 수 있도록 제한을 추가하십시오.

allowPorts = [
  { start = 20000, end = 20010 }
]
maxPortsPerClient = 5

대부분의 제공업체는 서버 내부의 ufw와 별개로 제어 패널에서 네트워크 방화벽을 운영합니다. sudo ufw status에서 규칙이 올바르게 설정된 것으로 보이는데도 타임아웃이 발생한다면, 보통 해당 방화벽에서 차단된 경우입니다. VPS에 실제로 필요한 ufw 규칙에서는 이 섹션에서 가정하는 기본 거부(default-deny) 설정을 단계별로 설명합니다.

VPS에서 실제 인증서로 HTTPS 종료하기

home.example.com에 대한 A 레코드를 VPS의 공인 IP로 지정합니다. 집 주소로 지정하지 마십시오. 집에는 지정할 주소가 없으며, 바로 그 문제를 해결하려는 것입니다.

# on the VPS
sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx

certbot이 작업할 수 있는 일치하는 server_name를 갖추도록, 먼저 일반 포트 80 블록으로 /etc/nginx/sites-available/home.example.com을 생성합니다.

server {
    listen 80;
    listen [::]:80;
    server_name home.example.com;
    location / { return 404; }
}
# on the VPS
sudo ln -s /etc/nginx/sites-available/home.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot certonly --nginx -d home.example.com

nginx -t는 파일 구문 분석이 완료되면 nginx: configuration file /etc/nginx/nginx.conf test is successful을 출력합니다. 매번 리로드하기 전에 실행하십시오. nginx는 리로드가 실패하면 이전 설정을 계속 실행하므로, 잘못된 편집은 아무것도 하지 않은 것처럼 보일 수 있습니다.

WebSocket 업그레이드에는 http 레벨의 map이 하나 필요합니다. /etc/nginx/conf.d/upgrade.conf에 추가하십시오.

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

이제 사이트 파일을 실제 파일로 교체합니다.

server {
    listen 80;
    listen [::]:80;
    server_name home.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name home.example.com;

    ssl_certificate     /etc/letsencrypt/live/home.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/home.example.com/privkey.pem;

    client_max_body_size 512m;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        $connection_upgrade;
        proxy_read_timeout 3600s;
    }
}

여기서 proxy_set_header Host $host;은 선택 사항이 아닙니다. frp의 HTTP vhost는 Host 헤더를 기준으로 라우팅하며, 클라이언트 설정의 customDomains 목록과 대조합니다. 이 헤더를 생략하면 nginx는 Host: 127.0.0.1을 전송하고, frp는 해당 이름에 대한 프록시를 찾지 못하여 방문자는 애플리케이션 페이지 대신 frp로부터 404 오류를 받게 됩니다. nginx 리버스 프록시 블록의 모든 라인 설명에서 다른 헤더들이 수행하는 역할을 다룹니다.

# on the VPS
sudo nginx -t && sudo systemctl reload nginx
sudo certbot renew --dry-run

드라이 런(dry run)은 90일 뒤, 즉 사용자가 확인하지 않을 때 갱신이 정상적으로 작동할 것임을 증명합니다. 이를 위해서는 포트 80에 접근할 수 있어야 하며, ufw 규칙이 존재하는 이유가 바로 이것입니다.

홈 측 설정: 권한 없는 서비스로 frpc 실행

frpcfrps을 설치했던 것과 동일한 버전 및 체크섬 검증 절차를 거쳐 홈 서버에 설치합니다. 그 후 동일한 frp 사용자와 /etc/frp 디렉터리를 생성합니다. /etc/frp/frpc.toml 파일을 다음과 같이 작성합니다.

serverAddr = "vps.example.com"
serverPort = 7000

auth.method = "token"
auth.token = "PASTE_THE_SAME_TOKEN_HERE"

transport.tls.enable = true
loginFailExit = false

proxies = [
  { name = "home-app", type = "http", localIP = "127.0.0.1", localPort = 3000, customDomains = ["home.example.com"] }
]

이 파일에서는 키의 순서가 중요하며, 이는 단순히 스타일 때문이 아닙니다. TOML은 테이블 헤더 아래에 있는 모든 키를 해당 테이블에 할당하므로, 프록시 테이블 헤더 아래에 serverAddr과 같은 최상위 설정을 작성하면 frp가 무시하는 프록시 설정으로 잘못 인식될 수 있습니다. 위와 같이 프록시 목록을 인라인 배열로 작성하면 이러한 문제를 방지할 수 있으며, 모든 최상위 키가 명확하게 최상위 수준으로 유지됩니다.

type = "http"은 이 프록시를 자체적인 공용 TCP 포트를 점유하는 대신 vhost 리스너를 통해 라우팅하도록 합니다. 이것이 방화벽 규칙을 4개로 유지할 수 있었던 이유입니다. customDomains에는 nginx가 Host 헤더로 전달하는 호스트 이름이 포함되어야 하므로, VPS의 IP 주소가 아닌 home.example.com을 사용해야 합니다.

loginFailExit = false는 보이는 것보다 중요합니다. 기본값은 true이며, 이는 첫 번째 로그인 시도에 실패할 경우 frpc를 즉시 종료시킵니다. ISP 연결이 완료되기 전에 부팅이 끝나는 홈 서버 환경에서는 서비스가 죽은 상태로 방치될 수 있습니다. 이 값을 false로 설정하면 VPS가 응답할 때까지 frpc가 계속 재시도합니다.

/etc/systemd/system/frpc.service 파일을 다음과 같이 작성합니다.

[Unit]
Description=frp reverse tunnel client
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=frp
Group=frp
ExecStart=/usr/local/bin/frpc -c /etc/frp/frpc.toml
Restart=always
RestartSec=10s
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true

[Install]
WantedBy=multi-user.target
# on the home box
sudo chown root:frp /etc/frp/frpc.toml
sudo chmod 640 /etc/frp/frpc.toml
sudo -u frp frpc verify -c /etc/frp/frpc.toml
sudo systemctl daemon-reload
sudo systemctl enable --now frpc
sudo journalctl -u frpc -n 20 --no-pager

클라이언트가 연결되면 다음과 같은 실행 ID가 로그에 기록됩니다.

login to server success, get run id [3a1f9c2b7d4e5f60]

브라우저에서 https://home.example.com을 열면 홈 서버의 127.0.0.1:3000에서 실행 중인 애플리케이션에 접속할 수 있습니다. 클라이언트 측의 Restart=always은 의도된 설정입니다. 홈 네트워크 연결은 끊어질 수 있으며, 서비스는 관리자의 개입 없이 자동으로 복구되어야 하기 때문입니다.

대시보드를 공용 인터페이스에서 격리하기

webServer.addr = "127.0.0.1"를 사용하면 대시보드는 VPS 내부에서만 응답합니다. 포트를 개방하는 대신 로컬 포트 포워딩을 사용하여 노트북에서 접속하십시오:

# on your laptop
ssh -N -L 7500:127.0.0.1:7500 you@vps.example.com

http://127.0.0.1:7500을 열고 frps.toml에서 확인한 webServer.userwebServer.password로 로그인합니다. 이 페이지에는 연결된 모든 클라이언트와 각 프록시의 트래픽 카운터가 나열되므로, "현재 홈 박스가 연결되어 있는가"라는 질문에 가장 빠르게 답할 수 있습니다. ssh 세션을 종료하면 대시보드는 다시 접근할 수 없는 상태가 됩니다.

터널이 수행하지 않는 작업

이 부분을 두 번 읽으십시오. 많은 사용자가 여기서 보안 사고를 겪기 때문입니다. 터널은 비공개 서비스를 공용 인터넷에서 접근할 수 있게 만들어 줄 뿐, 서비스에 접근하는 사용자를 인증해주지는 않습니다. https://home.example.com가 확인되면 며칠 내로 스캐너가 이를 찾아낼 것입니다. 도메인 이름을 누구에게도 알리지 않았더라도 결과는 같습니다. 인증서 투명성 로그(Certificate transparency logs)는 발급받은 모든 호스트 이름을 공개하므로, certbot이 성공하는 즉시 해당 이름은 공개된 정보가 됩니다.

외부에 노출하는 모든 서비스는 자체적인 인증 수단을 갖추어야 합니다. 애플리케이션에 속도 제한(rate limiting)이 적용된 정상적인 로그인 기능이 있다면 다행입니다. 만약 공유 비밀번호를 사용하거나 로그인 기능이 아예 없다면, VPS에서 해당 서비스 앞단에 인증 프록시를 배치하십시오. 애플리케이션 앞단에 oauth2-proxy를 배치하는 것이 일반적인 해결책이며, 이는 nginx와 frp vhost 사이에서 터널 양단에 아무런 변경 없이 적용할 수 있습니다.

frps.toml의 토큰은 터널을 보호할 뿐, 애플리케이션을 보호하지 않습니다. 이 토큰은 외부인이 귀하의 VPS에 자신의 프록시를 등록하는 것을 막아줄 뿐입니다. 의도적으로 게시한 호스트 이름으로 443 포트를 통해 들어오는 요청에 대해서는 아무런 보호 조치를 하지 않습니다.

두 가지 습관을 유지하는 것이 좋습니다. 토큰은 스스로 만료되지 않으므로, 양쪽 파일을 모두 수정하고 두 서비스를 재시작하여 주기적으로 교체하십시오. 또한 frp를 항상 최신 상태로 유지하십시오. 이 바이너리는 외부를 향한 현관문과 같습니다. v0.71.0 릴리스 노트에는 클라이언트가 보낸 잘못된 값으로 인해 서버 패닉이 발생하는 사례가 기록되어 있는데, 이는 논리적으로 분석하기보다 즉시 패치해야 하는 유형의 버그입니다.

실패 유형 및 확인되는 메시지

클라이언트가 연결되지 않습니다. journalctl -u frpcconnect to server error:을 반복하며 연결 시간 초과(dial timeout)가 발생합니다. 포트 7000으로 아무것도 도달하지 않는 상태입니다. VPS의 ufw를 확인하고, 제어판에서 제공업체의 네트워크 방화벽을 확인한 뒤, getent hosts vps.example.com로 도메인 이름이 올바르게 해석되는지 확인하십시오.

토큰이 잘못되었습니다. 클라이언트가 다음과 같이 명확하게 메시지를 출력합니다:

login to the server failed: token in login doesn't match token from configuration

토큰을 다시 복사하십시오. 끝에 포함된 줄 바꿈 문자나, 셸에서 따옴표로 묶지 않아 빈 문자열로 확장된 $가 이러한 문제의 대부분을 유발합니다. 이것이 바로 TOML 파일에서 openssl rand -base64 32 출력을 따옴표로 묶어야 하는 이유입니다.

터널은 연결되었으나 브라우저에서 404 오류가 발생합니다. frpc 로그에는 성공적인 로그인이 기록되어 있고 대시보드에도 프록시가 표시되지만, 페이지는 스타일이 적용되지 않은 채 404 오류를 반환합니다. 이는 frp가 해당 Host 헤더에 대한 프록시를 찾지 못했음을 의미합니다. nginx와 TLS를 모두 우회하여 VPS에서 vhost를 직접 테스트하십시오:

# on the VPS
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: home.example.com' http://127.0.0.1:8080/

해당 명령에서 404가 반환된다면 customDomains 설정이 잘못된 것입니다. 다른 응답 코드가 반환된다면 요청이 nginx로부터 올바른 Host을 전달받지 못한 것입니다.

nginx에서 502 오류가 발생합니다. nginx는 응답하고 있으나 frp가 응답하지 않는 상태입니다. VPS에서 sudo ss -lntp | grep 8080를 실행하면 frps가 127.0.0.1:8080에서 대기 중인지 확인할 수 있습니다. 결과가 출력되지 않는다면 frps가 중지되었거나 frps.toml 파일에 vhostHTTPPort이 설정되지 않은 것입니다.

애플리케이션이 모든 방문자를 로컬 사용자로 인식합니다. 애플리케이션 로그에 모든 요청이 127.0.0.1로 기록됩니다. frp는 X-Forwarded-For를 설정하고 nginx가 여기에 값을 추가하므로, 실제 클라이언트 주소는 해당 헤더에 포함되어 있습니다. 애플리케이션이 이 헤더를 신뢰하도록 설정하십시오. 애플리케이션이 IP 주소 기반으로 속도 제한(rate limit)을 수행하는 경우 이 과정을 생략하지 마십시오. 그렇지 않으면 인터넷의 모든 방문자가 하나의 버킷을 공유하게 됩니다.

긴 요청이 60초 후에 끊깁니다. 업로드나 스트리밍 응답이 도중에 중단됩니다. 이는 터널 문제가 아니라 nginx의 기본값인 proxy_read_timeout 때문입니다. 위 블록의 설정은 이를 3600초로 늘려줍니다. client_max_body_size은 업로드 크기 제한과 관련이 있으며, 기본값인 1 MB를 초과하는 요청은 413 오류로 거부됩니다.

정상 작동하다가 공유기 재부팅 후 연결이 끊깁니다. frpc 유닛의 Restart=always 설정과 loginFailExit = false을 통해 해결할 수 있습니다. sudo systemctl is-enabled frpc를 실행하여 enabled가 출력되는지 확인하십시오.

FAQ

CGNAT 환경인지 어떻게 확인합니까?

라우터 관리 페이지에 표시된 WAN 주소와 동일한 네트워크 내부에서 curl -4 -s https://ifconfig.me이 보고하는 주소를 비교하십시오. 두 주소가 다르고 라우터의 WAN 주소가 100.64.0.0/10 범위에 있다면, ISP가 CGNAT(Carrier-Grade NAT)를 운영 중인 것입니다. 해당 범위는 RFC 6598 공유 주소 공간이며 이러한 목적으로 존재합니다. 일부 ISP는 WAN 측에서 10.0.0.0/8을 대신 사용하기도 하는데, 이는 같은 의미입니다. 두 주소가 일치한다면 공인 IP를 사용 중인 것이므로, 포트 포워딩을 설정하면 작업이 완료됩니다.

리버스 터널을 위해 도메인 이름이 필요합니까?

여기서 설명하는 HTTPS 설정을 위해서는 도메인 이름이 필요합니다. 인증서는 호스트 이름에 대해 발급되며, frp의 HTTP vhost는 Host 헤더를 기준으로 요청을 라우팅하므로 양쪽 끝단이 동일한 이름을 사용해야 합니다. 숫자 포트를 사용하는 원시 TCP 프록시는 도메인 없이 VPS의 IP만으로도 작동하지만, 이 경우 인증서와 호스트 이름 기반 라우팅을 사용할 수 없어 하나의 공인 포트에서 하나의 서비스만 제공할 수 있습니다.

공용 VPS에서 frp를 실행해도 안전합니까?

제어 포트만 노출하고 인증을 설정하면 안전합니다. 양쪽 끝단에서 auth.token을 임의의 값으로 설정하고 서버 측에서 transport.tls.force = true을 설정하십시오. 그 후 proxyBindAddr = "127.0.0.1"를 설정하여 frp가 프록시를 위해 여는 포트가 인터넷에 직접 노출되지 않도록 하고, 대시보드는 webServer.addr = "127.0.0.1"에 두어 SSH 로컬 포워딩을 통해서만 접근하도록 하십시오. frp는 공인 주소에서 수신 대기하는 프로세스이므로, 새 버전이 릴리스되면 즉시 바이너리를 업데이트하십시오.

왜 아무도 제 ssh -R 포워딩 포트에 접근할 수 없습니까?

sshd는 기본적으로 GatewayPorts no 설정이 적용되어 있어, 원격 포워딩은 VPS의 루프백 인터페이스에만 바인딩됩니다. 따라서 VPS 자체에서 실행하는 curl은 작동하지만, 외부에서 시도하는 curl은 타임아웃이 발생합니다. 올바른 해결책은 포워딩을 루프백에 유지하고 그 앞단에 443 포트로 nginx를 배치하는 것입니다. GatewayPorts yes을 설정하여 인증서와 TLS 없이 원시 포트를 공개하는 것은 해결하려는 문제보다 더 위험합니다.

frp를 사용해야 합니까, 아니면 Tailscale이나 WireGuard 같은 메시 VPN을 사용해야 합니까?

본인의 기기들만 접근하면 되는 경우에는 메시 VPN을 사용하십시오. 이 경우 아무것도 공개되지 않으며 외부에서 스캔할 수 있는 공인 호스트 이름도 존재하지 않습니다. 웹훅 수신기나 VPN 클라이언트를 설치하지 않은 사람들과 공유해야 하는 페이지처럼, 모든 브라우저에서 접근 가능한 공인 HTTPS 주소가 필요할 때는 frp를 사용하십시오. 두 방식은 서로 다른 포트를 사용하여 각자의 역할을 수행하며 하나의 VPS에서 공존할 수 있습니다.

#frp#cgnat#nat#tunnel#reverse-proxy