노트북에도 걸 수 있는 주소를

p4는 노드에게 걸어서 일을 준다. NAT 뒤의 기계에는 걸 주소가 없다. 그리고 대부분의 기계가 NAT 뒤에 있다. 그런 기계 하나를 합류시키기까지 무엇이 필요했는지, 그리고 합류한 날의 실측이다.

막힌 것은 의지가 아니라 방향이었다. p4는 노드*에게* TCP 연결을 열어 일을 전달한다. 노드가 스스로 연 연결은 영수증 전용이라, 그 위로 온 Data 프레임은 peer_closed("unexpected frame on outbound hop")으로 끊긴다. 노트북이 바깥으로 터널을 열고 그 터널로 일감을 받는 일은 일어나지 않는다. 의지로도, 재시도로도, 더 나은 클라이언트로도.

제약

기여자의 기계는 기본적으로 도달 불가다

가정의 기계는 NAT 뒤에 앉아 있다. 인터넷으로 나갈 수는 있지만 인터넷이 들어올 수는 없다. 우리 플릿이 양쪽을 다 보여준다. 사무실 GB10은 공인 주소가 직접 붙어 있고, MI250 두 대는 하나의 공용 출구 뒤 192.168.20.x에 있다. MI250이 특이한 게 아니다. 기여자의 기계가 바로 그 모습이다.

p4에는 라우팅 테이블도, 랑데부 서비스도, 홀펀칭도 없다. deliver_outbound는 이벤트 봉투에서 주소를 꺼내 connect를 부른다. 그게 전부이고, 랙 안에서는 합리적인 설계다. 다만 노트북이 만족시킬 수 있는 설계가 아닐 뿐이다.

해법

바깥으로 연 연결 하나를 붙들고 있기

릴레이가 노드를 대신해 공인 주소를 갖는다. 노드는 그쪽으로 나가는 연결 하나만 유지하고 아무것도 듣지 않는다. 누군가 그 공인 주소로 걸면 바이트는 노드가 이미 들고 있던 연결을 타고 내려온다. p4의 양쪽 끝은 평범한 주소로 향하는 평범한 소켓을 볼 뿐, 어느 쪽도 릴레이의 존재를 알지 못한다.

거는 쪽이 보는 것과 노드가 실제로 돌리는 것

caller  ->  tcp://relay:43100        # 평범한 p4 주소
            |
            +-- relay      공개. 바이트를 넘기되 해석하지 않는다
                  |
                  +-- tunnel   노드가 스스로 연 바깥 연결 하나
                        |
                        +-- p4-agent  127.0.0.1:42031, 루프백만 듣는다

릴레이는 p4를 읽지 않는다. 페이로드를 바이트 단위로 넘기고 파싱하지 않으므로 LOAD와 INSPECT를 구분하지 못한다 — 그리고 구분할 수 있으면 안 된다. 트래픽을 이해하는 순간 그것을 바꿀 수 있는 물건이 되기 때문이다.

이것이 동시에 보안 경계인 이유

포트포워딩은 애초에 선택지가 아니었다

p4에는 어떤 인증도 없다. TLS도, 토큰도, 허용목록도 없어서 에이전트의 포트에 닿는 누구나 NODE_LOAD·NODE_UNLOAD·INSPECT를 보낼 수 있다. 기여자에게 집 공유기의 포트를 그쪽으로 열라고 요구하는 것은 변호할 수 없고, 가장 뻔한 해법이 틀린 해법인 이유가 그것이다.

릴레이를 거치면 노드는 아무것도 듣지 않는다. 바깥으로 연 연결 하나를 유지하고, 그 연결이 바이트를 나르기 전에 운영자 지갑을 증명한다 — 정산 게이트웨이가 이미 쓰는 것과 같은 ed25519/base58 서명이라, 노드가 등록하는 신원이 보상 원장이 키로 쓰는 신원과 같다. 도달성과 인증은 결국 같은 답을 갖는다.

동작한 날

주장이 아니라 측정

6 ms
플릿 호스트에서 NAT 뒤 Mac 까지
28 ms
에이전트가 자기 규칙을 적용하기까지
512 KiB
바이트 단위로 동일하게 왕복
0
노트북에서 연 포트 수

데이터센터의 MI250 호스트가 tcp://34.50.62.159:43100으로 걸어 NAT 뒤 맥북에서 도는 p4 에이전트에 6 ms 만에 닿았다. 28 ms 뒤 에이전트가 연결을 끊었다. 첫 프레임이 Hello가 아니었기 때문이다 — 자기 프로토콜 규칙을, 조금 전까지 연락조차 닿지 않던 기계가 스스로 적용한 것이다. 그 거절이 증거다. 릴레이는 페이로드를 파싱하지 않으므로 그것을 만들어낼 수 없다.

검사결과
512 KiB 왕복바이트 동일, 177 ms
256 KiB 왕복바이트 동일, 108 ms
동시 발신자 3개스트림이 서로 섞이지 않음
서명 없는 등록거부
재사용된 챌린지거부
나머지 절반

노드에는 돌릴 것이 있어야 한다

엔진이 뒤에 없으면 도달성은 무의미한데, 데스크탑 앱은 그것을 배포한 적이 없었다. 빌드 디렉터리 세 곳에서 p4-agent 바이너리를 찾았고, 이미 p4를 손수 컴파일한 기계에서만 발견됐다. 이제 앱이 배포되는 플랫폼마다 자기 바이너리를 싣는다.

  • macOS: 두 아키텍처를 합친 universal 바이너리. Mac 앱은 universal로 패킹되고 리소스는 양쪽 슬라이스에 그대로 복사되므로, arm64만 넣으면 Intel Mac 사용자는 멀쩡해 보이는데 노드가 켜지지 않는 앱을 받는다 — 개발자가 가진 적 없는 하드웨어에서만 보이는 실패다.
  • Windows: 크로스 컴파일하고, 탐색이 p4-agent.exe를 찾도록 했다. 확장자를 빼먹는 것이 윈도우 빌드가 자기 에이전트를 못 찾는 경로다.
  • 패키징마다 에이전트 빌드가 먼저 돌므로, 에이전트 없는 릴리스는 만들어질 수 없다.

아직 되지 않는 것. 노드는 이제 돌고 도달된다. 다만 추론으로 버는 것은 여전히 없다. 엔진은 스테이지별 작업 기록을 내보내지만 그것을 모으는 것이 없고, 정산 게이트웨이의 기여도 엔드포인트는 — 어떤 노드도 자기 기여를 스스로 적을 수 없도록 올바르게 막혀 있는 채로 — 한 번도 호출된 적이 없다. 도는 것과 보상받는 것은 별개의 문제이고, 지금 풀린 것은 앞의 하나뿐이다.