원문 캡처 · github.com
원문 캡처 · github.com

Steam Frame의 무선 어댑터가 실제로 하는 일

Steam Frame의 어댑터가 라우터라고 생각했다면 그렇지 않다. 어댑터는 헤드셋이 자체적으로 만든 숨겨진 핫스폿에 연결하는 Wi-Fi 6E 동글이다. 이 글은 어댑터 없이 PC의 Wi-Fi 카드로 헤드셋 핫스폿에 연결하는 도구 steamframe-link를 만든 과정과 사용 조건을 설명한다.

어댑터 없이 연결하기

처음부터 설명하면, 글쓴이는 Steam Frame으로 Half-Life: Alyx를 플레이하려 했지만 어댑터를 다른 사람 집에 두고 왔다. 집에 있는 GL.iNet 여행용 라우터는 2.4GHz만 지원해 게임을 제대로 하기 어려웠다.

처음에는 PC 메인보드의 Wi-Fi 6E 카드를 사용했다. 연결은 훨씬 나아졌지만 지연 시간이 갑자기 튀는 문제가 남았다. 글쓴이가 파악한 바로는 Frame은 두 개의 Wi-Fi 연결을 동시에 사용해 스트리밍할 수 있으며, 밸브는 이를 멀티링크(Multilink)라고 부른다. 한 연결에서 문제가 생겨도 다른 연결이 같은 순간에 함께 끊기지 않으면 지연 급증을 줄일 수 있다는 설명이다. 한 연결만 사용하면 모든 지연 급증이 그대로 체감된다.

Frame은 두 연결을 쓸 수 있지만 PC에는 Wi-Fi 카드가 하나뿐이었다. 별도의 Wi-Fi 6E 카드도 없다고 생각했지만, 노트북의 Wi-Fi를 활용할 수 있었다. 글쓴이는 맥북을 이더넷으로 네트워크에 연결한 뒤 핫스폿으로 사용했다. 설정은 간단하지 않았고 버그도 많았지만, 작동한 뒤에는 책상 아래 안테나에 묶이지 않고 옮길 수 있는 액세스 포인트(AP)를 얻었다.

맥북 핫스폿도 무선 장치가 하나뿐이었고 정식 어댑터도 없었다. 그래서 PC의 Wi-Fi 카드를 Frame에 연결할 방법을 찾아야 했다. 당시에는 어댑터가 Frame 쪽에서 만든 숨겨진 핫스폿에 불과하다는 사실을 몰랐다. 설정 메뉴를 뒤져 두 번째 무선 장치가 연결될 대상을 바꿀 수 있는지 찾아봤지만 방법을 찾지 못했다.

Frame은 리눅스 기반이고, 밸브는 Frame에서 SSH를 활성화하는 옵션을 제공한다. SSH로 접속해 보니 softapmanager가 사용되고 있었고, Frame은 frame_XXXXXXXX라는 숨겨진 네트워크와 32자 비밀번호를 만들었다. 이 네트워크는 별도의 wlanap 인터페이스를 사용한다. PC에서 해당 네트워크에 연결하자 어느 정도 작동했다.

멀티링크 스트리밍은 두 무선 장치를 모두 사용해 작동했다. 하지만 메인보드의 Wi-Fi 카드가 밸브 공식 어댑터로 인식되는지는 Frame의 화면에 따라 달랐다. 한 화면에는 밸브 어댑터가 연결되지 않았다고 나왔지만, 연결한 뒤 다른 화면을 보면 Wi-Fi와 밸브 어댑터를 통해 연결됐다고 표시됐다. Frame의 표시가 서로 달랐고, SteamVR은 처음부터 어댑터가 연결됐다고 표시했다.

자격 증명을 찾아가는 과정

글쓴이는 PC를 Frame의 숨겨진 핫스폿에 연결하되 어댑터와 SSH는 쓰지 않는 도구를 만들고 싶었다. 어댑터를 한 번이라도 연결한 적이 있으면 Steam이 네트워크 자격 증명을 NetworkManager 프로필에 저장해 둔다. 하지만 목표는 어댑터를 한 번도 연결하지 않은 상태에서도 자격 증명을 얻는 것이었다.

처음에는 Steam의 로컬 Remote Play 프로토콜을 통해 자격 증명을 가져오려 했다. PC에서 이 프로토콜을 처리하는 steamclient.so를 직접 프로그램에 불러온 뒤 실행 중인 Steam에 연결해 요청하는 방식이었다. 과정은 다음과 같았다.

  1. steamclient.so를 불러오고 CreateInterface를 호출해 IPC 파이프로 실행 중인 Steam에 연결했다.
  2. 필요한 PairViaWifiAP 같은 메서드를 찾았다. 메서드는 있었지만 외부에 공개되지 않았고, 건드리도록 만들어지지 않은 비공개 C++ 인터페이스에 들어 있었다.
  3. 공개된 함수가 없어 가상 함수 테이블(vtable)을 이용했다. 객체 포인터와 함수 포인터 테이블에서 올바른 위치를 골라 함수를 호출하는 방식이었다.
  4. 필요한 함수를 찾으려고 실행 중 ELF를 읽고 메서드 이름을 불러오는 명령어를 검색했다. 엉뚱한 함수를 호출하지 않도록 호출하는 함수마다 해시도 계산했다.
  5. Steam이 업데이트되자 해시 16개 중 15개가 바뀌었고, PairViaWifiAP는 55번 슬롯에서 61번 슬롯으로 이동했다.
  6. 해시를 무시하는 옵션을 추가했다. 사실상 “그냥 믿어 달라”는 설정이었다.
  7. Steam 베타에서는 GetRemoteClientCount가 없다는 오류가 났다. 실제로는 함수가 있었지만 컴파일러가 문자열을 먼저 R13에 불러온 뒤 RCX로 옮겼고, 검색기는 RCX만 살폈다.
  8. 레지스터 이동을 따라가는 실제 명령어 디코더를 작성했다. 이론상 지난 1년간 찾을 수 있었던 모든 Steam 버전을 인식한다.

글쓴이는 이 코드를 엉망이고 지금까지 작성한 코드 중 최악에 가까웠다고 설명했다. 그래도 어댑터를 연결한 적이 없는 상태에서 Steam으로부터 자격 증명을 가져오는 데 성공했다고 생각했다.

문제는 다른 PC에서는 작동하지 않았다는 점이다. 어댑터를 연결한 적이 없는 두 번째 PC에서 여러 steamclient.so 버전을 디버깅한 뒤, 글쓴이는 작동 방식에 대한 자신의 이해가 틀렸다는 사실을 알아냈다. Frame이 비밀번호를 갖고 있고 Steam이 이를 요청하는 것이 아니라, 두 번째 PC의 Steam은 자격 증명을 받은 적도 요청한 적도 없었다. 첫 번째 PC에서는 실제 어댑터를 마지막으로 페어링했을 때 저장된 자격 증명을 가져온 것이었다. 새로 받아 온 것이 아니라 기억하고 있었던 셈이다.

단서는 오류 메시지에 있었다. 글쓴이가 Remote Play 프로토콜에 직접 처음 접속했을 때는 “unauthorized”라는 응답을 받았다. 같은 LAN에 있다는 이유만으로 다른 사람의 핫스폿에 접근할 수는 없고, 페어링이 필요하다는 뜻이었다. 나중에 정상적으로 인증한 뒤 다시 요청하자 이번에는 “No dongle present”라는 메시지가 나왔다. 비밀번호를 주거나 접근을 거부한 것이 아니라 동글이 없다고 응답했다.

자격 증명은 Steam이 만든다

확인 결과, Frame이나 어댑터가 자격 증명을 설정하는 것이 아니었다. Steam이 어댑터의 정보를 이용해 SSID와 비밀번호를 만들고 Remote Play 프로토콜로 Frame에 전달한다. Frame은 그 자격 증명을 사용해 핫스폿을 연다.

따라서 도구가 할 일은 어댑터 없이 Steam과 같은 절차를 수행하는 것이었다. 기본 설정에서는 임의의 SSID와 비밀번호를 만들며, 직접 지정할 수도 있다. 이는 Frame의 Wi-Fi 작동 방식과 반대 방향으로 진행된다. 일반적으로는 액세스 포인트가 자격 증명을 정하고 클라이언트가 연결하지만, 여기서는 PC가 자격 증명을 정하고 실제 액세스 포인트인 Frame이 전달받은 설정으로 네트워크를 연다.

이에 따라 steamclient.so를 파고들던 코드는 모두 삭제했다. 도구는 대신 Remote Play LAN 프로토콜을 직접 사용한다.

  1. Steam과 같은 방식으로 일반 LAN에서 Frame을 찾는다.
  2. PC와 Frame을 Steam에서 한 번 페어링할 때 Steam이 저장한 키로 연결한다. 따라서 Steam에서 한 차례 페어링해야 한다.
  3. Frame에 wifi_dongle_present 플래그를 설정해 동글이 있다고 알린다.
  4. Frame에 SSID와 비밀번호를 전달한다.
  5. Frame이 자격 증명을 적용했다고 확인할 때까지 기다린다.

그 뒤 NetworkManager가 PC의 Wi-Fi 카드를 Frame의 새 핫스폿에 연결한다. 동글이나 SSH, 실행 중 어셈블리 분석은 필요하지 않다.

사용 조건과 명령어

도구를 사용하려면 다음 조건을 충족해야 한다.

  • NetworkManager를 사용하는 리눅스가 필요하다.
  • Wi-Fi 카드가 6GHz와 WPA3를 지원해야 하며, 사용 중인 규제 도메인도 이를 허용해야 한다. 글쓴이의 설정에서 Frame 핫스폿은 6GHz, 160MHz, WPA3-SAE를 사용했으므로 오래된 2.4GHz 전용 카드는 사용할 수 없다.
  • 도구를 실행하는 사용자 계정으로 Steam을 실행하고 Remote Play를 활성화해야 한다.
  • PC와 Frame을 Steam에서 미리 페어링해야 한다.
  • Frame이 켜져 있고 PC와 같은 LAN에 있어야 한다.
  • 직접 빌드하려면 Rust, OpenSSL 3 개발 라이브러리, pkg-config가 필요하다. 또는 Actions에서 빌드된 파일을 내려받을 수 있다.
sh
cargo build --release
./target/release/steamframe-link devices

# Generate credentials, send them to the Frame, and save them locally.
./target/release/steamframe-link set-creds

# Connect your WiFi card using the saved credentials.
./target/release/steamframe-link setup --iface wlan0

직접 SSID와 비밀번호를 정하려면 다음과 같이 실행한다.

sh
./target/release/steamframe-link set-creds --ssid frame_MYPC --psk-stdin

set-creds는 Frame의 핫스폿을 바꾸므로 현재 무선 연결이 끊길 수 있다. PC에 만들어지는 프로필은 기본 경로(default route)가 되지 않으므로 Frame에 연결해도 일반 인터넷 연결은 유지된다.

비밀번호는 출력 가능한 ASCII 문자 8~63자, SSID는 제어 문자가 없는 1~32바이트여야 한다. --psk PASSWORD를 사용할 수도 있지만 비밀번호가 셸 기록에 남는다. 저장된 자격 증명은 $XDG_STATE_HOME/steamframe-link/CLIENT_ID.json에 보관하며, 기본 경로는 ~/.local/state/steamframe-link/다. 생성한 자격 증명은 Frame에 보내기 전에 저장하므로 연결이 끊겨도 생성된 비밀번호를 잃지 않는다. 저장된 값을 다시 보내려면 set-creds --saved, 화면에 표시하려면 set-creds --saved --reveal을 사용한다.

도구는 Frame이 성공 응답을 보내거나 일치하는 네트워크 해시를 보고한 경우에만 자격 증명이 설정됐다고 판단한다. 연결이 끊기거나 시간이 초과된 것은 성공으로 처리하지 않으며, 확인되지 않은 자격 증명으로는 setup을 실행할 수 없다.

Frame이 여러 대라면 devices 명령이 보여 주는 ID를 --client ID와 함께 지정한다. 네트워크 검색이 막혀 있으면 --host FRAME_LAN_IPV4를 쓸 수 있다. --steam-dir PATH는 다른 Steam 설치 경로를 지정하며, --timeout SECONDS는 설정 제한 시간을 지정한다. 기본값은 30초다. setup --provision --iface wlan0을 사용하면 Frame 설정과 PC 연결을 한 번에 처리한다.

자격 증명을 이미 알고 있다면 setup --iface wlan0 --ssid NAME --psk-stdin으로 Frame을 변경하지 않고 PC를 설정할 수 있다. setup --from-nm은 Steam의 기존 NetworkManager 프로필을 복사한다. 단순히 setup만 실행하는 경우에는 Steam 실행이나 LAN 검색이 필요하지 않다.

--no-connect는 PC 프로필을 저장하되 활성화하지 않고, --no-autoconnect는 자동 연결을 끈다. --iface를 지정하지 않으면 도구가 밸브 어댑터를 자동으로 찾는다. 프로필은 WPA3-SAE와 필수 관리 프레임 보호를 사용하고, 숨겨진 SSID 검색과 절전 비활성화, 무선 장치의 영구 MAC 주소를 설정한다. 호환되는 환경을 위해 --wpa2 옵션도 제공한다. PC 프로필은 up --iface wlan0, down --iface wlan0, remove로 관리할 수 있다.

SteamVR에서 겪은 문제

글쓴이는 도구를 설명하기 전에 리눅스의 SteamVR 사용 경험도 덧붙였다. 프랑켄슈타인처럼 구성한 연결을 쓰지 않아도 연결에 어려움이 있었고, 때로는 연결까지 1분이 걸렸다. 기다리는 동안 연결 버튼을 누르면 누른 횟수가 저장됐다가 나중에 한꺼번에 실행됐다.

SteamVR과 Half-Life: Alyx에서 여러 차례 충돌도 봤지만, 원인이 Frame인지 리눅스인지 알 수 없었다. 한 충돌은 Alyx의 손상된 뮤텍스(poisoned mutex)와 관련된 것으로 추적됐다. 그 뒤 SDL 창이 메인 스레드에서 대기해 게임이 멈추거나 충돌한 것처럼 보였지만 실제로는 그 창을 기다리고 있었다고 설명했다.

쓸 만했을까

어댑터를 두고 온 곳에서 가져오는 것보다 빨랐느냐는 질문에 글쓴이는 “아니다. 비교도 안 된다”고 답했다. 대신 Steam Frame과 Remote Play 프로토콜, steamclient.so에 대해 원치 않을 만큼 많이 알게 됐으며, 이제 다른 사람도 자신의 6GHz 또는 7GHz Wi-Fi 카드를 어댑터처럼 사용할 수 있기를 바란다고 덧붙였다. 이 도구의 사용 조건은 6GHz 지원으로 적혀 있으며, 7GHz 언급은 글쓴이의 마지막 소감에 나온다.