WebShell
WebShell 은 플랫폼 나머지가 사용하는 같은 geminio 터널을 통해 모든 등록된 edge 에 도달하는 브라우저 대면 터미널입니다. 별도의 SSH bastion 없음, 점프 호스트 없음, 인바운드 포트 없음. Edge 는 계속 다이얼 아웃; 매니저는 셸 I/O 용 멀티플렉스 스트림 클래스를 엽니다.
사용 사례:
- 에이전트가 수정을 제안; "edge-prod-04 에 셸 열기" 를 클릭하고 SPA 를 떠나지 않고 변경 확인.
- 벤더 / 계약자가 VPN 등록 없이 한 호스트에 일회성 확인 필요.
- Incident 대응: 모든 명령은 발원 세션의 감사 행과 함께 기록됨.
아키텍처
browser ──WebSocket──> manager:/v1/webshell/ws
│
├─ Router.Register(sessionID, sink, ActiveSession)
│
└─ geminio Stream (shell class)
│
▼
edge agent
└─ pty.Start("/bin/bash")매니저 측 라우터는 internal/manager/biz/webshell/router.go 에. sessionID → Sink 디렉터리 유지: WebSocket 핸들러가 연결 시 등록, 터널 인커밍 디스패처가 edge 의 출력 / exit push 를 올바른 브라우저로 라우팅.
// internal/manager/biz/webshell/router.go:57
type Router struct {
mu sync.RWMutex
sinks map[string]Sink
meta map[string]*ActiveSession // sessionID → metadata
stdoutBytes sync.Map // sessionID → *uint64
}HTTP / WebSocket 핸들러는 옆 internal/manager/server/webshell 에 있어 라우터는 HTTP 비종속이고 단위 테스트 가능한 상태를 유지.
두 스트림 클래스
geminio 터널이 멀티플렉스:
- 컨트롤 클래스 — JSON RPC (기능 실행, 플러그인 시그널링, 알림 evaluator 프로브).
- 셸 클래스 — 원시 바이트 스트림 (WebShell 세션당 하나,
tail -ffollower 당 하나 등).
터널 레벨에서 분할이 중요한 이유는 셸 I/O 가 버스트 있고 프레임이 없기 때문; 컨트롤 RPC 와 혼합하면 후자를 굶주리게 만듭니다. 각 클래스는 자체 backpressure 예산을 가짐.
세션 메타데이터
각 라이브 세션은 ActiveSession 가짐:
// router.go:37
type ActiveSession struct {
SessionID string
OngridUserID uint64
SSHUser string
DeviceID uint64
EdgeID uint64
StartedAt time.Time
LastInputAt time.Time // updated on every browser → edge frame
}LastInputAt 이 모든 키 입력 (Router.TouchInput) 에 틱. 유휴 타임아웃 watchdog 이 최근 입력 없이 구성 제한보다 오래된 세션을 축출 — "실행 중인 명령과 함께 브라우저 탭을 닫았다" 누수를 방어.
감사 녹화
두 레이어:
- 헤더 행 —
webshell_sessions테이블: 누가, 언제, 어느 edge, exit code, 총 in/out 바이트. - 스트림 녹화 — 매니저 측
Recorder인터페이스가 와이어를 가로지르는 모든 바이트 (양방향, 타임스탬프) 를 받아/var/lib/ongrid/webshell-recordings/<session_id>.cast아래의 asciinema 호환 cast 파일에 append. Admin/admin/webshell페이지가 재생.
Recorder 인터페이스는 의도적으로 좁음 — 프로덕션은 파일 sink 사용; 테스트는 가짜 주입; 향후 클라우드 blob 백엔드가 나머지 스택을 건드리지 않고 들어옴.
동시성 제한
사용자별 상한: Router.CountByUser 가 WebSocket open 핸들러에서 호출됨; 상한 초과 연결은 HTTP 429 로 거부. 기본 상한 5 (구성 가능). Edge 별 상한은 runaway 에이전트가 100 동시 셸을 여는 것을 방어.
세션 종료
세 경로가 세션 종료:
- 브라우저 닫기 — WebSocket disconnect 가 edge 로 전파, edge 는 pty 에
kill -HUP. - 관리자 종료 — admin SPA 가
Sink에서Killer.Kill(reason="admin terminated")호출, edge 로 닫기 터널링. 세션의 exit 행에 reason 기록. - 유휴 축출 — watchdog 이
LastInputAt이 상한을 초과한 세션에Kill("idle timeout")발화.
// router.go:50
type Killer interface {
Kill(reason string)
}매니저 측 핸들러가 Sink 를 등록할 때 Killer 를 설치. Opt-in 하는 모든 Sink 는 관리자 종료 가능; 나머지는 브라우저 닫기만 가능.
Role 게이팅
WebShell 은 admin role 에 게이트 (ADR-022 RBAC). user role 은 에이전트와 채팅 가능하지만 셸을 열 수 없음; viewer 는 과거 세션의 녹화를 읽을 수 있지만 새로 열 수 없음. 게이트는 WebSocket 업그레이드 전에 HTTP 핸들러 진입에서 실행.
같이 보기
- 기능 —
bash기능은 WebShell 의 일회성 등가물 (단일 명령, pty 없음). 같은 감사 기질. - Edge install — WebShell 이 도달할 수 있도록 호스트 의 edge 에이전트를 띄우기.
- 아키텍처 — geminio 터널이 어디에 위치하는지.