구독을 가져온 뒤 config.json을 더 깊이 살펴보고 싶은 Mac 사용자를 위한 글입니다. 먼저 로컬 인바운드, 직접 연결, 차단만 포함한 전체 설정으로 구조를 파악한 다음 포트, 아웃바운드 태그, 규칙 순서와 DNS를 확인합니다. 예시 포트는 설명을 위해 지정한 값이며 모든 클라이언트의 기본 설정을 의미하지 않습니다.
config.json과 v2rayN 설정의 차이
config.json은 코어가 읽는 설정 파일이고, v2rayN은 노드, 구독, 실행 상태를 관리하는 데스크톱 클라이언트입니다. 두 항목은 서로 다른 계층에 있습니다. 클라이언트는 인터페이스 설정을 코어 실행 설정으로 변환하거나 사용자 지정 설정을 관리할 수 있습니다. 파일 이름이 config.json이라는 이유만으로 해당 파일을 현재 프로세스가 읽고 있다고 단정할 수는 없습니다.
이 글에서는 V2Ray의 기존 JSON 설정 구조를 사용합니다. Xray와 V2Ray는 비슷한 필드가 있지만 프로토콜 지원과 세부 매개변수는 완전히 같지 않습니다. 설정을 살펴보기 전에 클라이언트 실행 로그에서 실제로 시작된 코어, 설정 파일 경로, 시작 오류를 확인하세요. 한 코어에 맞는 프로토콜 매개변수를 다른 코어에 그대로 붙여 넣지 마세요.
이 흐름을 보면 시스템 프록시의 역할도 알 수 있습니다. 시스템 프록시 설정을 따르는 앱은 요청을 로컬 인바운드로 보냅니다. 코어만 단독으로 실행한다고 모든 앱이 자동으로 이 경로를 이용하는 것은 아닙니다. 시스템 프록시를 사용하지 않는 앱에는 해당 앱의 프록시 옵션이나 트래픽을 명시적으로 전달하는 다른 방법이 필요합니다.
inbounds: 요청을 받아들이며, 리스닝 주소와 포트, 로컬 프록시 프로토콜을 지정합니다.outbounds: 요청을 처리하며, 직접 연결·프록시 서버·차단 방식을 지정합니다.routing: 아웃바운드를 선택하고, 조건에 맞는 요청을 지정된 태그로 전달합니다.log와dns: 각각 로그와 코어의 DNS 동작을 제어하며, 앞의 세 항목을 대신하지 않습니다.
전체 예시: 인바운드 2개, 아웃바운드 2개
아래는 로컬에서 설정 구조를 파악하기 위한 예시입니다. SOCKS와 HTTP 진입점을 제공하고, 기본적으로 직접 연결하며, 예시 도메인 example.com과 하위 도메인을 차단합니다. 원격 프록시 서버는 설정되어 있지 않으므로 구독 노드 설정으로 사용할 수 없습니다. 테스트 전에 실행 중인 클라이언트가 이 두 포트를 사용하고 있지 않은지 확인하세요.
{
"log": {
"loglevel": "warning"
},
"inbounds": [
{
"tag": "local-socks",
"listen": "127.0.0.1",
"port": 10808,
"protocol": "socks",
"settings": {
"auth": "noauth",
"udp": false
}
},
{
"tag": "local-http",
"listen": "127.0.0.1",
"port": 10809,
"protocol": "http",
"settings": {}
}
],
"outbounds": [
{
"tag": "direct",
"protocol": "freedom",
"settings": {}
},
{
"tag": "blocked",
"protocol": "blackhole",
"settings": {}
}
],
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"domain": [
"domain:example.com"
],
"outboundTag": "blocked"
},
{
"type": "field",
"ip": [
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16"
],
"outboundTag": "direct"
}
]
}
}
가장 바깥쪽 구조를 보면 인바운드, 아웃바운드, 규칙은 모두 배열입니다. 대괄호 안에 객체를 여러 개 넣을 수 있으며, 객체 사이는 쉼표로 구분하고 마지막 항목 뒤에는 쉼표를 붙이지 않습니다. 일반적인 JSON 도구와 호환되도록 저장할 때는 큰따옴표를 사용하고 주석은 넣지 마세요. 포트도 따옴표로 감싼 문자열이 아니라 숫자로 작성해야 합니다.
inbounds: 리스닝 주소, 포트, 인바운드 태그
listen은 연결을 받을 네트워크 인터페이스를 지정합니다. 예시의 127.0.0.1은 로컬 기기에서만 연결할 수 있습니다. 이를 0.0.0.0으로 바꾸면 모든 IPv4 인터페이스에서 리슨하지만, 다른 기기에서 반드시 접속할 수 있다는 뜻은 아닙니다. 방화벽, 네트워크 격리, 인증도 고려해야 합니다. 로컬에서만 사용할 때는 루프백 주소를 유지하는 편이 접근 범위를 제어하기 쉽습니다.
SOCKS 인바운드
- 태그
local-socks- 리스닝 주소
127.0.0.1:10808- 프로토콜
socks- 인증
noauth, 로컬에서만 사용- UDP
false, 이 예시에서는 비활성화
앱에서 SOCKS 프록시를 선택해야 합니다. 이 포트를 HTTP 프록시 포트로 입력하면 안 됩니다.
HTTP 인바운드
- 태그
local-http- 리스닝 주소
127.0.0.1:10809- 프로토콜
http- 매개변수
settings: {}
HTTP 프록시 요청과 CONNECT 터널 요청을 받을 수 있습니다. 이 진입점 자체가 TLS 암호화 리스너를 의미하지는 않습니다.
macOS에서는 ‘시스템 설정’ → ‘네트워크’ → 현재 네트워크 연결 → ‘세부사항’ → ‘프록시’에서 주소와 포트를 확인할 수 있습니다. 이 예시의 HTTP 인바운드를 사용한다면 필요에 따라 웹 프록시와 보안 웹 프록시에 127.0.0.1:10809을 지정할 수 있습니다. SOCKS 인바운드의 포트는 10808입니다. 문제를 진단하는 동안에는 필요한 방식 하나만 활성화해 설정 간 간섭을 피하세요.
tag는 코어 설정 내부에서 사용하는 식별자이며, 서버 주소도 아니고 모든 앱이 알아보는 이름도 아닙니다. 라우팅의 inboundTag로 요청의 출처를 구분할 수 있습니다. 예를 들어 한 규칙은 local-http만 처리하고 다른 규칙은 local-socks만 처리하도록 설정하면 진입점별로 정책을 지정할 수 있습니다.
outbounds: 프로토콜이 동작을 결정하고 tag가 라우팅을 연결합니다
예시의 freedom은 직접 연결 아웃바운드로, 현재 기기에서 대상에 직접 접속합니다. blackhole은 요청을 차단합니다. 실제 원격 프록시 아웃바운드에는 해당 프로토콜에 맞는 서버 주소, 포트, 인증 정보와 필요한 전송 계층 설정이 있어야 합니다. tag만 proxy로 바꿔도 직접 연결이 프록시 연결로 바뀌지는 않습니다.
| 필드 또는 프로토콜 | 의미 | 확인할 사항 |
|---|---|---|
tag | 아웃바운드 내부 이름 | 규칙의 outboundTag와 정확히 일치하는지 |
protocol | 아웃바운드 구현 선택 | 실행 중인 코어가 해당 프로토콜을 지원하는지 확인 |
settings | 해당 프로토콜 전용 매개변수 | 프로토콜마다 구조가 다르므로 같은 형식을 적용할 수 없음 |
streamSettings | 전송 방식과 전송 보안 설정 | 서버 요구 사항과 항목별로 일치하는지 |
freedom | 대상에 직접 연결 | 이 기기의 실제 네트워크 출구를 사용하는지 |
blackhole | 조건에 맞는 요청 차단 | 정상적인 트래픽이 잘못 매칭되지 않는지 확인 |
일반적인 설정에서는 라우팅 규칙에 일치하지 않는 요청에 outbounds 배열의 첫 번째 아웃바운드가 사용됩니다. 이 예시에서는 direct가 첫 번째 항목이므로 규칙에 일치하지 않는 요청은 직접 연결됩니다. 클라이언트가 생성한 파일을 읽을 때 이 순서는 특히 중요합니다. 파일에 프록시 아웃바운드가 있다고 해서 규칙에 일치하지 않는 트래픽도 반드시 그 아웃바운드를 사용하는 것은 아닙니다.
핵심: 기본 아웃바운드를 먼저 확인한 다음 개별 규칙을 살펴보세요
‘왜 여전히 직접 연결되는지’ 확인할 때는 먼저 outbounds의 첫 번째 항목을 살펴보고, 규칙에 일치한다면 outboundTag도 확인하세요. 노드 이름이나 인터페이스 색상만으로 실제 네트워크 출구를 판단하지 마세요.
routing: 첫 번째 일치 규칙, 조건 조합, 도메인 정책
routing.rules는 위에서 아래 순서로 확인합니다. 규칙 하나가 일치하면 지정된 아웃바운드를 사용하고, 더 구체적인 규칙을 찾기 위해 아래로 계속 내려가지 않습니다. 따라서 명확한 예외 규칙은 보통 범위가 넓은 규칙보다 앞에 둡니다. 배열 순서는 설정 동작의 일부이므로 순서를 바꾸면 문법 오류 없이도 네트워크 출구가 달라질 수 있습니다.
- 요청 대상이
www.example.com인 경우: 첫 번째 규칙의domain:example.com이 기본 도메인과 하위 도메인에 일치하므로blocked로 전달됩니다. - 요청 대상이
192.168.1.20인 경우: 첫 번째 규칙에는 일치하지 않지만 두 번째 규칙의 사설 IP 주소 대역에는 일치하므로direct로 전달됩니다. - 대상이 위 조건에 해당하지 않는 경우: 어떤 규칙에도 일치하지 않으므로 첫 번째 아웃바운드인
direct를 사용합니다.
하나의 규칙에서 서로 다른 조건 필드는 보통 모두 충족해야 합니다. 같은 필드의 후보 값은 어느 하나에 일치하면 됩니다. 예를 들어 아래 규칙은 HTTP 인바운드에서 들어오고 대상 포트가 25인 TCP 요청만 차단합니다. SOCKS 인바운드로 들어온 요청은 포트가 같더라도 모든 조건을 충족하지 않습니다.
{
"type": "field",
"inboundTag": ["local-http"],
"network": "tcp",
"port": "25",
"outboundTag": "blocked"
}
이 객체는 기존 routing.rules 배열에 넣고 필요한 위치에 배치해야 합니다. 전체 설정은 아닙니다. 필드 유형도 다르다는 점에 유의하세요. 인바운드 리스너의 port는 숫자지만 라우팅 규칙의 port는 포트나 범위를 문자열로 지정합니다. 예: "80,443".
AsIs: 라우팅 단계에서 IP 규칙을 위해 도메인을 별도로 조회하지 않습니다. 요청 대상이 원래 IP 주소라면 IP 규칙에 일치할 수 있습니다.IPIfNonMatch: 도메인 요청이 기존 조건에 일치하지 않으면 주소를 조회한 뒤 라우팅 규칙을 다시 적용합니다.IPOnDemand: 규칙을 확인하다 IP가 필요한 경우 도메인을 조회합니다.
geosite: 또는 geoip: 유형의 조건을 사용할 때는 코어가 해당 데이터 파일을 읽을 수 있는지, 지정한 항목 이름이 존재하는지 확인해야 합니다. 이 글에서는 외부 데이터 의존성을 배제하고 규칙 동작을 확인할 수 있도록 구체적인 도메인과 CIDR 대역을 사용합니다. routing.domainStrategy를 직접 연결 아웃바운드의 이름이 같거나 비슷한 DNS 정책 옵션과 혼동하지 마세요.
DNS와 sniffing: 규칙에서 확인할 수 있는 대상 정보
도메인 규칙이 일치하는지는 인바운드에서 어떤 대상 정보를 받는지에 따라 달라집니다. HTTP 프록시 요청과 CONNECT에는 보통 도메인이 포함됩니다. SOCKS 요청에는 도메인이 전달될 수도 있고, 앱이 미리 조회한 IP 주소가 전달될 수도 있습니다. 앱이 로컬에서 먼저 도메인을 조회하면 코어에는 주소만 전달될 수 있으므로, 코어가 원래 도메인을 알고 있다고 단정할 수 없습니다.
curl --proxy socks5h://127.0.0.1:10808 --connect-timeout 5 https://example.com/
curl --proxy http://127.0.0.1:10809 --connect-timeout 5 https://example.com/
첫 번째 명령의 socks5h는 curl이 도메인 조회를 SOCKS 프록시에 맡기도록 합니다. socks5를 사용하면 curl이 로컬에서 대상 도메인을 조회합니다. 이 예시에서는 해당 도메인 요청이 차단되어야 하므로 명령이 실패하는 것이 예상된 결과입니다. 구체적인 오류 메시지는 연결 단계에 따라 달라집니다. 여기서 5는 연결 제한 시간(초)이며 성능 측정값이 아닙니다.
dns객체는 코어의 DNS 기능을 설정하며, macOS 시스템 DNS 설정을 자동으로 변경하지 않습니다.sniffing은 일부 트래픽에서 HTTP Host나 TLS 서버 이름 등의 정보를 식별할 수 있지만, 모든 연결에서 도메인을 알아낼 수 있는 것은 아닙니다.- sniffing 활성화 여부, 대상 정보 덮어쓰기 여부, 앱이 도메인을 먼저 조회했는지에 따라 최종 매칭 결과가 달라집니다.
- 문제를 진단할 때는 프록시 유형과 테스트 명령을 고정한 뒤 한 번에 변수 하나만 바꾸고 로그를 비교하세요.
설정 변경 순서와 자주 발생하는 오류 확인 방법
먼저 백업하고 현재 적용 중인 파일을 확인한 다음 수정하세요. v2rayN은 노드 전환, 구독 업데이트, 코어 재시작 시 실행 설정을 다시 생성할 수 있습니다. 생성된 파일을 임시로 수정하는 방법은 문제를 확인할 때는 쓸 수 있지만 규칙을 장기간 보존하기에는 적합하지 않습니다. 유지해야 하는 변경 사항은 클라이언트의 해당 설정이나 관리 대상 사용자 지정 설정에 반영하세요.
- 코어와 경로를 확인합니다. 시작 로그에서 코어 이름, 불러온 파일, 오류가 발생한 줄을 확인하세요. 경로에 공백이 있으면 터미널 명령에서 파일 경로를 따옴표로 감싸야 합니다.
- JSON 구조를 확인합니다. macOS 터미널에서 아래 명령을 실행하세요. 이 명령은 JSON을 파싱할 뿐, 코어가 각 필드를 지원하는지는 검증하지 않습니다.
- 리스닝 포트를 확인합니다.
10808과10809를 다른 프로세스가 사용 중인지 확인하고 앱 설정에 지정된 포트와 일치하는지 살펴보세요. - 태그 참조를 확인합니다.
outboundTag가 실제로 존재하는 아웃바운드를 가리키는지,inboundTag가 인바운드 태그와 일치하는지 하나씩 확인하세요. - 다시 불러오고 검증합니다. 클라이언트에서 코어를 재시작하거나 설정을 다시 불러온 뒤 정상적으로 시작되는지 확인하세요. 그런 다음 규칙에 일치하는 대상과 일치하지 않는 대상을 각각 테스트합니다.
plutil -lint -s -r -- config.json
lsof -nP -iTCP:10808 -sTCP:LISTEN
lsof -nP -iTCP:10809 -sTCP:LISTEN
plutil은 여기서 JSON 문법을 확인하는 데 사용합니다. 문법 검사를 통과해도 프로토콜 이름의 오타, 필수 매개변수 누락, 코어 비호환성으로 인해 시작에 실패할 수 있습니다. 리스너 확인 결과가 비어 있다고 해서 설정 문법 오류라고 단정할 수도 없습니다. 프로세스가 시작되었는지, 다른 파일을 읽고 있지는 않은지도 확인하세요.
| 증상 | 우선 확인할 항목 | 다음 조치 |
|---|---|---|
| 코어 시작 직후 종료 | JSON 문법, 필드 호환성, 필수 매개변수 | 시작 로그에서 처음 발생한 실제 오류 확인 |
| 주소가 이미 사용 중이라는 메시지 | 인바운드 포트와 중복 실행 프로세스 | 포트를 점유한 프로세스를 찾아 중지하거나 포트 변경 |
| 규칙이 적용되지 않음 | 규칙 순서, 대상 형식, 인바운드 태그 | 고정된 프록시 명령으로 재현하고 로그와 비교 |
| 재시작 후 변경 사항이 사라짐 | 생성된 파일을 수정했는지 확인 | 설정을 클라이언트의 관리 메뉴로 옮기기 |
| 브라우저는 되지만 다른 앱은 실패 | 앱이 시스템 프록시를 따르는지 확인 | 앱 자체의 프록시 유형과 포트 확인 |
핵심: 한 번에 한 계층씩 수정하세요
먼저 인바운드가 요청을 받는지 확인하고, 다음으로 아웃바운드가 작동하는지 확인한 뒤 라우팅을 조정하세요. 문제를 진단하는 동안에만 로그 상세도를 잠시 높이고, 작업을 마치면 원래 수준으로 되돌리세요. 로그와 설정을 공유하기 전에는 구독 주소, 인증 정보, 개인 대상 정보를 삭제하세요.