Xray는 노드가 연결되어 있는지만 표시하는 도구가 아닙니다. StatsService와 API 인바운드를 함께 구성하면 각 프록시 출구의 업로드·다운로드 통계, 연결 성공 여부, 최근 응답 상태를 자동화 스크립트에서 읽을 수 있습니다. 스크립트는 이 정보를 바탕으로 지연 시간이 낮고 오류가 적은 노드를 선택한 뒤, 장애가 발생하면 다른 출구로 전환하도록 만들 수 있습니다.
다만 Xray API가 노드의 품질을 알아서 평가해 주는 것은 아닙니다. 통계 수집 대상, API 접근 주소, 라우팅 태그, 전환 기준과 복구 조건을 운영자가 명확하게 정해야 합니다. 특히 API 포트를 외부에 공개하거나 모든 프로세스에 관리자 권한을 부여하면 자동화 편의성보다 보안 위험이 커질 수 있으므로, 로컬 루프백 또는 별도의 관리 네트워크에서만 사용해야 합니다.
Xray 1.8.x 계열과 호환되는 JSON 구조를 기준으로 StatsService, API 인바운드, 정책 통계를 구성하고, gRPC API에서 노드별 상태를 읽어 자동 전환하는 흐름을 설명합니다. v2rayN에서 생성된 설정을 점검하는 방법, 직접 수정할 JSON 예제, Python 스크립트의 실패·복구 기준과 운영 시 주의할 점까지 단계별로 확인합니다.
Xray API 자동 전환의 전체 구조
자동 전환에는 네 가지 요소가 필요합니다. 첫째, 여러 서버를 각각 다른 outbound 태그로 등록해야 합니다. 둘째, 통계 수집 기능이 각 출구의 트래픽 카운터를 기록해야 합니다. 셋째, API 인바운드가 로컬에서 StatsService와 HandlerService 요청을 받아야 합니다. 넷째, 스크립트가 측정 결과를 해석해 현재 사용할 출구를 바꾸거나, 새로운 설정으로 코어를 재시작해야 합니다.
StatsService의 기본 카운터는 특정 시간의 실제 지연 시간이나 패킷 손실률을 직접 제공하지 않습니다. 일반적으로 outbound>>proxy-a>>traffic>>uplink처럼 태그별 누적 바이트와 요청 처리 상태를 조회합니다. 따라서 성능 기반 선택을 하려면 스크립트가 일정 간격으로 테스트 URL에 요청을 보내 응답 시간과 오류를 별도로 측정해야 합니다. 통계 카운터는 “트래픽이 실제로 해당 출구를 통과했는가”를 확인하는 보조 자료로 사용하는 편이 정확합니다.
StatsService와 API 인바운드 설정하기
아래 예제는 API 요청을 외부에 노출하지 않고 로컬 루프백 주소에서만 받는 구성을 보여 줍니다. 기존 v2rayN 설정을 직접 덮어쓰기보다 먼저 백업 파일을 만들고, 코어 설정의 api, stats, policy, inbounds 영역을 확인하세요. v2rayN 버전에 따라 그래픽 인터페이스가 일부 항목을 다시 생성할 수 있으므로 최종적으로 Xray가 실제로 읽는 JSON을 검사해야 합니다.
{
"log": {
"loglevel": "warning"
},
"api": {
"tag": "api",
"services": [
"StatsService",
"HandlerService"
]
},
"stats": {},
"policy": {
"levels": {
"0": {
"statsUserUplink": true,
"statsUserDownlink": true
}
},
"system": {
"statsInboundUplink": true,
"statsInboundDownlink": true,
"statsOutboundUplink": true,
"statsOutboundDownlink": true
}
},
"inbounds": [
{
"tag": "api-in",
"listen": "127.0.0.1",
"port": 10085,
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1"
},
"listen": "127.0.0.1"
}
],
"outbounds": [
{
"tag": "proxy-a",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "node-a.example",
"port": 443,
"users": [
{
"id": "UUID-REPLACE",
"encryption": "none"
}
]
}
]
}
},
{
"tag": "proxy-b",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "node-b.example",
"port": 443,
"users": [
{
"id": "UUID-REPLACE",
"encryption": "none"
}
]
}
]
}
},
{
"tag": "direct",
"protocol": "freedom"
},
{
"tag": "block",
"protocol": "blackhole"
}
],
"routing": {
"rules": [
{
"type": "field",
"inboundTag": [
"api-in"
],
"outboundTag": "api"
},
{
"type": "field",
"domain": [
"geosite:category-ads-all"
],
"outboundTag": "block"
},
{
"type": "field",
"network": "tcp,udp",
"outboundTag": "proxy-a"
}
]
}
}
실제 환경에서는 API 인바운드가 반드시 api 출구로 라우팅되어야 합니다. API 요청이 일반 프록시 출구로 빠지면 스크립트가 연결되지 않거나 API 응답이 다시 프록시 경로로 순환할 수 있습니다. 또한 위 JSON의 address, 포트, UUID와 전송 계층 항목은 예시이므로 구독에서 제공된 실제 서버 정보와 일치시켜야 합니다. 설정을 저장한 뒤 Xray의 구성 검증 기능 또는 클라이언트의 코어 시작 로그에서 JSON 문법과 라우팅 오류를 확인하세요.
StatsService에서 노드 상태 읽는 방법
Xray의 API는 gRPC 기반으로 동작하며, 일반적인 REST 주소에 브라우저로 접속하는 방식과 다릅니다. 자동화 스크립트는 API 클라이언트를 사용해 StatsService/QueryStats를 호출하고, 조회할 통계 이름을 전달해야 합니다. 노드 태그가 proxy-a라면 출구 통계의 이름은 보통 outbound>>proxy-a>>traffic>>uplink과 outbound>>proxy-a>>traffic>>downlink 형태입니다.
{
"name": "outbound>>proxy-a>>traffic>>uplink",
"value": 184320
}
환경에 따라 통계 이름이나 제공되는 값이 달라질 수 있으므로 특정 이름을 하드코딩하기 전에 전체 통계 목록을 한 번 조회하는 것이 안전합니다. 태그를 바꾼 뒤에도 스크립트의 목록, 라우팅 규칙, 통계 조회 이름을 함께 수정해야 합니다. 출구 이름에 공백이나 임의의 한글을 사용하면 로그 검색과 스크립트 처리가 어려워지므로 proxy-a, proxy-b처럼 짧고 고유한 영문 태그를 권장합니다.
각 출구를 통해 동일한 HTTPS 대상에 요청을 보내 응답 시간과 실패 여부를 계산합니다.
적합:성능 기반 선택, 정기적인 품질 비교
출구별 누적 업로드·다운로드 값을 확인해 실제 트래픽 통과 여부와 사용량을 검증합니다.
적합:활성 노드 확인, 트래픽 모니터링
핸드셰이크 실패, 연결 시간 초과와 출구 선택 결과를 확인할 수 있지만 숫자 기반 비교에는 제한이 있습니다.
적합:원인 분석, 전환 결과 추적
Python으로 상태 확인과 전환 로직 만들기
운영 자동화는 먼저 “현재 출구가 실패했는가”와 “다음 출구가 정말 더 나은가”를 분리해서 판단해야 합니다. 현재 노드의 단일 요청이 일시적으로 느렸다는 이유만으로 즉시 전환하면 노드가 계속 교체되는 플래핑이 발생합니다. 예를 들어 10초 간격으로 세 번 연속 실패하면 장애로 판정하고, 새 후보가 두 번 연속 성공하면서 현재 출구보다 20% 이상 빠를 때만 전환하는 방식이 안정적입니다.
-
설정 백업
현재 Xray JSON을 별도 파일로 복사하고, 자동화 스크립트가 수정할 출구 태그와 라우팅 규칙을 정합니다. 원본 파일을 바로 변경하지 말고
config.active.json과 날짜가 붙은 백업 파일을 구분하세요. -
API 로컬 확인
Xray를 재시작한 뒤 API 주소를
127.0.0.1:10085로 고정합니다. 외부 IP에 바인딩하지 말고, API 서비스가StatsService와HandlerService를 모두 포함하는지 시작 로그에서 확인하세요. -
후보 측정
각 노드에 동일한 HTTPS 대상과 동일한 타임아웃을 적용합니다. 예를 들어 5초 타임아웃으로 세 번 요청하고 중앙값을 사용하면 한 번의 DNS 지연이나 순간적인 서버 부하가 결과를 과도하게 흔드는 것을 줄일 수 있습니다.
-
실패 누적
현재 출구의 실패 횟수를 메모리에 기록합니다. 세 번 연속 실패했을 때만 전환하며, 성공한 요청이 들어오면 실패 카운터를 0으로 되돌립니다. 재시작 때 상태가 초기화되므로 마지막 선택 출구는 작은 상태 파일에 저장하는 편이 좋습니다.
-
전환 검증
새 출구를 적용한 뒤 실제 웹 요청, DNS 조회와 StatsService의 업로드 카운터를 함께 확인합니다. 카운터가 증가하지 않으면 화면상 선택만 바뀌었을 가능성이 있으므로 라우팅 태그와 코어 재로드 결과를 다시 점검하세요.
import time
import statistics
import subprocess
from urllib.request import build_opener, ProxyHandler, Request, urlopen
NODES = {
"proxy-a": ("127.0.0.1", 10808),
"proxy-b": ("127.0.0.1", 10818),
}
CHECK_URL = "https://example.com/"
TIMEOUT = 5
FAIL_LIMIT = 3
current = "proxy-a"
failures = {name: 0 for name in NODES}
def measure(node):
host, port = NODES[node]
proxy = "socks5h://{}:{}".format(host, port)
opener = build_opener(ProxyHandler({
"http": proxy,
"https": proxy
}))
samples = []
for _ in range(3):
started = time.monotonic()
try:
request = Request(CHECK_URL, headers={"User-Agent": "xray-health-check"})
with opener.open(request, timeout=TIMEOUT) as response:
response.read(128)
samples.append(time.monotonic() - started)
except Exception:
pass
return statistics.median(samples) if samples else None
def switch_outbound(node):
# 운영 환경에서는 여기서 HandlerService 또는 관리용
# 설정 재생성·코어 재로드 절차를 호출합니다.
print("switch request:", node)
while True:
latency = measure(current)
if latency is None:
failures[current] += 1
else:
failures[current] = 0
if failures[current] >= FAIL_LIMIT:
candidates = {}
for node in NODES:
if node != current:
result = measure(node)
if result is not None:
candidates[node] = result
if candidates:
best = min(candidates, key=candidates.get)
switch_outbound(best)
current = best
failures[current] = 0
time.sleep(240)
위 코드는 전환 판단의 뼈대만 보여 주는 예제입니다. socks5h를 사용하면 측정 대상의 도메인 조회도 프록시 측에서 처리되므로 로컬 DNS 결과가 지연 측정에 섞이는 것을 줄일 수 있습니다. 다만 Python 실행 환경에 SOCKS 지원이 없으면 해당 프록시 핸들러가 작동하지 않을 수 있으므로, 사용하는 라이브러리와 v2rayN의 실제 SOCKS 포트가 일치하는지 먼저 확인해야 합니다.
HandlerService 전환과 재시작 방식 비교
자동으로 “노드를 바꾼다”는 표현에는 두 가지 구현이 있습니다. 하나는 고정된 프록시 그룹이나 출구 선택 구조를 두고 HandlerService를 통해 현재 선택 대상을 변경하는 방식입니다. 다른 하나는 스크립트가 JSON을 다시 만든 뒤 Xray 프로세스를 검증·재시작하는 방식입니다. 전자는 연결 중단을 줄일 수 있지만 현재 사용 중인 코어 버전과 클라이언트가 해당 API 동작을 지원하는지 확인해야 합니다. 후자는 구현이 단순하지만 재시작 순간 기존 연결이 끊길 수 있습니다.
- HandlerService 기반: API를 통해 핸들러 또는 출구 선택을 변경할 때 사용합니다. 관리 API 주소를 로컬로 제한하고, 요청 전후 로그를 남겨 실제 라우팅 변경 여부를 확인해야 합니다.
- 설정 재생성 기반: 안정적으로 검증된 템플릿에 노드 정보를 넣고 새 JSON을 만든 뒤 코어를 재시작합니다. 문법 검증과 원자적 파일 교체가 중요합니다.
- 클라이언트 메뉴 기반: v2rayN이나 다른 클라이언트의 현재 선택 노드를 외부 프로그램이 직접 조작하는 방식입니다. UI 변경에 취약하므로 장기간 운영 자동화에는 적합하지 않습니다.
결론: 전환 속도보다 복구 안정성이 우선입니다
단일 타임아웃마다 노드를 바꾸지 말고, 현재 노드의 연속 실패와 후보 노드의 연속 성공을 각각 확인하세요. 3회 실패·2회 성공 같은 히스테리시스 기준을 두면 일시적인 무선 불안정이나 원격 서버의 짧은 지연 때문에 출구가 반복 교체되는 현상을 크게 줄일 수 있습니다.
자동 전환을 운영 환경에 적용하기 전 점검
API 포트는 Xray의 일반 프록시 포트가 아닙니다. API 인바운드를 0.0.0.0에 열어 두거나 방화벽 예외를 넓게 설정하면 외부 사용자가 통계 조회뿐 아니라 핸들러 변경 요청을 시도할 수 있습니다. 기본값은 127.0.0.1로 유지하고, 스크립트도 같은 장치에서 실행하세요. 별도 관리 서버가 필요하다면 방화벽에서 관리 서버의 고정 주소만 허용하고, 평문 API가 공용망을 지나지 않도록 구성해야 합니다.
오류: failed to dial API server
원인 및 해결 방법:API 인바운드가 실행되지 않았거나 스크립트가 다른 포트에 연결하고 있습니다. Xray 로그의 api-in 바인딩 주소와 스크립트의 10085 포트를 비교하세요.
오류: stats query returned no matching item
원인 및 해결 방법:조회 이름의 출구 태그가 실제 outbound 태그와 다르거나 정책 통계가 활성화되지 않은 상태입니다. 태그의 대소문자와 outbound>>태그>>traffic>>uplink 형식을 다시 확인하세요.
오류: selected node changed but traffic is unchanged
원인 및 해결 방법:스크립트의 상태만 바뀌고 라우팅 규칙이나 코어의 실제 핸들러는 바뀌지 않았을 수 있습니다. 테스트 요청의 로그, 새 출구 카운터 증가 여부와 활성 라우팅 태그를 함께 확인하세요.
최종 검증은 노드 선택 화면만 보고 끝내지 마세요. 첫째, 의도한 출구의 업로드 카운터가 증가하는지 확인합니다. 둘째, 장애를 재현하거나 테스트 대상의 연결을 차단해 세 번 연속 실패 후 전환이 발생하는지 봅니다. 셋째, 원래 노드를 복구한 뒤 두 번 연속 성공했을 때 불필요한 즉시 복귀가 아니라 정한 복구 정책에 따라 안정적으로 돌아오는지 확인합니다. 마지막으로 스크립트 종료, Xray 재시작, 네트워크 단절 상황에서도 안전한 기본 노드와 직접 연결 정책이 유지되는지 점검하세요.