Mosh: 네트워크가 바뀌어도 끊기지 않는 원격 셸

Mosh: 네트워크가 바뀌어도 끊기지 않는 원격 셸

카페에서 서버 작업을 하다가 노트북을 덮고 지하철을 탄 뒤 다시 열었을 때, 터미널이 그대로 멈춰 있는 걸 보신 적 있으신가요? 🤔

몇 초쯤 기다리다가 결국 Ctrl+C를 눌러보고, 그래도 반응이 없어서 창을 닫고 다시 접속합니다. 아까 어느 디렉터리에 있었는지 기억을 더듬어 다시 이동하고요. 와이파이에서 테더링으로 갈아탈 때도, 사무실에서 집으로 이동할 때도 똑같은 일이 반복됩니다.

SSH는 TCP 위에서 동작하는데, TCP 연결은 클라이언트의 IP 주소에 묶여 있습니다. IP가 바뀌는 순간 그 연결은 되살릴 방법이 없어요. 서버 입장에서는 전혀 다른 상대가 말을 거는 셈이니까요.

Mosh(Mobile Shell)는 바로 이 지점을 정면으로 공략한 원격 셸입니다. 네트워크가 바뀌어도, 몇 시간 동안 노트북을 덮어놨어도 세션이 그대로 살아 있어요.

Mosh가 뭔가요

Mosh는 MIT의 Keith Winstein이 2012년에 발표한 원격 터미널 프로그램입니다. 이름 그대로 이동하면서(mobile) 쓰는 셸을 목표로 만들어졌어요.

중요한 건 Mosh가 SSH를 대체하는 게 아니라 SSH 위에 얹혀서 시작한다는 점입니다. 접속할 때 인증은 그대로 SSH가 처리하고, Mosh는 그 SSH 연결을 통해 세션 키를 주고받은 뒤 자기만의 통신으로 갈아탑니다. 그래서 서버에 새로운 인증 체계를 만들 필요가 없고, 쓰던 공개 키와 설정을 그대로 씁니다.

GPLv3 라이선스의 자유 소프트웨어이고, 2012년 공개 이후 지금까지 보고된 보안 취약점이 없다는 점도 눈여겨볼 만합니다.

SSH와 무엇이 다른가요

가장 큰 차이는 연결을 유지하는 방식입니다. SSH는 바이트 스트림(byte stream)을 그대로 실어 나르는 반면, Mosh는 화면의 상태를 동기화합니다.

SSHMosh
전송 계층TCPUDP
전송 단위바이트 스트림터미널 화면 상태
IP 변경연결 끊김세션 유지
키 입력 반응서버 왕복 후 표시로컬에서 즉시 표시
포트 포워딩지원미지원
스크롤백지원미지원

이 차이가 왜 중요한지는 느린 회선에서 확연히 드러납니다. SSH로 지구 반대편 서버에 붙어 있으면 키를 누르고 나서 글자가 화면에 뜰 때까지 왕복 시간만큼 기다려야 하는데요. Mosh는 누른 키를 클라이언트가 먼저 화면에 그려놓고, 서버가 확인해주면 확정합니다.

설치하기

macOS에서는 Homebrew로 설치합니다.

brew install mosh
결과
==> Pouring mosh--1.4.0_40.arm64_tahoe.bottle.tar.gz
🍺  /opt/homebrew/Cellar/mosh/1.4.0_40: 16 files, 873.8KB

Ubuntu나 Debian 계열은 apt, Fedora는 dnf, Arch는 pacman으로 설치할 수 있어요.

sudo apt-get install mosh   # Ubuntu, Debian
sudo dnf install mosh       # Fedora
sudo pacman -S mosh         # Arch

클라이언트와 서버 양쪽 모두에 설치해야 합니다. 설치하면 mosh-clientmosh-server 두 실행 파일이 함께 깔리는데, mosh 명령은 이 둘을 이어주는 래퍼예요.

설치가 끝났는지는 버전으로 확인합니다.

mosh --version
결과
mosh 1.4.0 [build mosh 1.4.0]
Copyright 2012 Keith Winstein <mosh-devel@mit.edu>
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

기본 사용법

SSH와 거의 똑같습니다. 명령 이름만 바꾸면 돼요.

mosh user@example.com

이 한 줄이 실행되면 내부적으로는 SSH로 서버에 접속해 mosh-server를 띄우고, 서버가 알려준 UDP 포트와 세션 키로 mosh-client가 다시 붙습니다. SSH 연결은 이 시점에 역할을 마치고 닫힙니다.

전체 사용법은 다음과 같습니다.

mosh --help
Usage: /opt/homebrew/bin/mosh [options] [--] [user@]host [command...]
        --client=PATH        mosh client on local machine
                                (default: "mosh-client")
        --server=COMMAND     mosh server on remote machine
                                (default: "mosh-server")

자주 쓰는 옵션은 이 정도입니다.

  • -p PORT: 서버가 쓸 UDP 포트나 범위를 지정합니다. 방화벽에서 특정 포트만 열어둔 경우에 필요해요.
  • --ssh=COMMAND: 초기 접속에 쓸 SSH 명령을 통째로 바꿉니다. SSH가 22번이 아닌 포트를 쓸 때 이렇게 넘깁니다.
  • -n: 예측 입력을 끕니다. --predict=never와 같습니다.
  • -a: 빠른 회선에서도 예측 입력을 켭니다. --predict=always와 같습니다.
  • --no-init: 터미널 초기화 문자열을 보내지 않습니다. 접속 후 화면이 지워지는 게 싫을 때 씁니다.
  • --local: SSH를 거치지 않고 로컬에서 mosh-server를 띄웁니다. 동작을 확인해 볼 때 유용해요.

SSH 포트가 다르다면 이렇게 씁니다.

mosh --ssh="ssh -p 2222" user@example.com

원격 명령을 바로 실행할 수도 있는데, 이때는 --로 구분해줍니다.

mosh user@example.com -- tmux attach

세션을 강제로 끊고 싶으면 Ctrl-^를 누른 뒤 .를 입력합니다. Mosh는 서버가 응답하지 않아도 클라이언트가 계속 기다리기 때문에, 정말로 끝내고 싶을 때는 이 조합이 필요해요.

로밍: 네트워크가 바뀌어도 끊기지 않아요

Mosh의 간판 기능입니다. 와이파이에서 LTE로 바뀌든, 노트북을 덮었다가 몇 시간 뒤에 열든 세션이 그대로입니다.

원리는 의외로 단순합니다. Mosh 서버는 “이 클라이언트는 이 IP에 있다”고 고정해두지 않아요. 대신 인증된 패킷이 도착할 때마다 발신지 주소를 최신값으로 갱신합니다.

flowchart TB
    accTitle: Mosh가 클라이언트 IP 변경을 처리하는 흐름
    accDescr: 클라이언트가 와이파이에서 LTE로 전환해 발신지 IP가 바뀌어도, 서버는 인증에 성공하고 일련번호가 더 큰 패킷의 발신지로 목적지를 갱신해 같은 세션을 이어간다.

    subgraph client["클라이언트"]
        wifi["와이파이<br/>203.0.113.5"] --> lte["LTE<br/>198.51.100.9"]
    end

    lte -- "암호화된 UDP 패킷" --> verify{"인증 통과 &<br/>일련번호가 더 큰가?"}
    verify -- "예" --> update["목적지를<br/>198.51.100.9로 갱신"]
    verify -- "아니요" --> drop(["폐기"])
    update --> keep(["같은 세션 유지"])

클라이언트는 자기 IP가 바뀐 걸 몰라도 됩니다. 3초마다 하트비트(heartbeat)를 보내니까 이동한 직후 다음 하트비트에서 서버가 알아서 따라옵니다. NAT 뒤에 있어도 같은 방식으로 동작해요.

여기서 보안이 걱정될 수 있는데요. 발신지 IP만 보고 갱신하는 게 아니라 AES-128 OCB3로 인증에 성공한 패킷이면서 일련번호가 이전보다 큰 경우에만 갱신합니다. 세션 키를 모르면 남의 세션을 가로챌 수 없고, 옛 패킷을 다시 보내는 재전송 공격도 일련번호에서 걸립니다.

예측 입력: 키를 누르면 바로 보여요

Mosh는 클라이언트에서 터미널 에뮬레이터를 직접 돌립니다. 그래서 사용자가 키를 누르면 서버 응답을 기다리지 않고 화면에 먼저 그릴 수 있어요. 이걸 로컬 에코(local echo)라고 부릅니다.

예측이 확정되지 않은 글자는 밑줄로 표시됩니다. 서버 응답이 도착해서 예측이 맞았다고 확인되면 밑줄이 사라지고, 틀렸으면 서버가 보내온 실제 화면으로 교정됩니다. 그래서 지연이 심한 회선에서도 “지금 내가 뭘 쳤는지” 감각을 잃지 않아요.

기본값은 adaptive인데, 회선이 느릴 때만 예측을 켜는 모드입니다. 로컬 네트워크처럼 빠른 구간에서는 알아서 꺼지니 평소에는 신경 쓸 일이 없습니다.

mosh --predict=always user@example.com   # 항상 켜기
mosh --predict=never user@example.com    # 항상 끄기

재미있는 건 이 예측이 vim이나 emacs 같은 전체 화면 프로그램 안에서도 동작한다는 점입니다. Mosh가 화면 상태를 이해하고 있기 때문에 가능한 일이에요.

어떻게 동작하나요

Mosh의 핵심은 상태 동기화 프로토콜(State Synchronization Protocol, 이하 SSP)입니다. SSH가 “서버가 보낸 바이트를 하나도 빠짐없이, 순서대로” 전달하는 데 집중한다면, SSP는 “지금 화면이 어떤 상태인지”만 맞추면 된다고 봅니다.

이 관점의 차이가 실용적인 결과를 만듭니다. 로그가 폭포처럼 쏟아지는 상황에서 Ctrl+C를 눌렀다고 해봅시다. SSH는 이미 보낸 출력을 전부 전달해야 하니 밀린 내용이 다 흘러간 뒤에야 멈추는데요. Mosh는 중간 상태를 건너뛰고 최종 화면만 그리면 되기 때문에 즉시 반응합니다.

SSP는 양방향으로 두 벌이 돌아갑니다. 클라이언트에서 서버로는 키 입력을 빠짐없이 전달하고(TCP와 비슷한 의미), 서버에서 클라이언트로는 가장 최신 화면 상태를 목표로 삼아 중간 프레임을 건너뜁니다. 회선 상태에 따라 프레임 전송 빈도도 조절해서 버퍼가 쌓이는 걸 막아요.

알아둬야 할 한계

Mosh가 SSH를 완전히 대체하지 못하는 이유들입니다.

스크롤백이 없습니다. Mosh는 보이는 화면만 동기화하기 때문에 터미널 위로 스크롤해서 지나간 출력을 볼 수 없어요. 이건 꽤 불편한데, tmuxZellij 같은 터미널 멀티플렉서를 함께 쓰면 해결됩니다.

포트 포워딩과 에이전트 포워딩이 안 됩니다. ssh -L로 원격 DB 포트를 로컬로 끌어오는 작업이나 ssh -A로 키를 전달하는 작업은 Mosh로 할 수 없습니다. X11 포워딩도 마찬가지고요. 이런 작업이 필요할 때는 SSH를 그대로 쓰면 됩니다.

UDP 포트가 열려 있어야 합니다. 기본적으로 60000번부터 61000번 사이의 UDP 포트를 씁니다. 사내망이나 클라우드 보안 그룹에서 이 대역이 막혀 있으면 접속되지 않아요. 방화벽 규칙을 넓게 열기 부담스럽다면 -p로 포트를 하나만 지정해서 그것만 여는 방법이 있습니다.

mosh -p 60001 user@example.com

네트워크 자체를 열기 어려운 환경이라면 Tailscale 같은 오버레이 네트워크를 깔고 그 위에서 Mosh를 쓰는 방법도 있습니다.

UTF-8 로케일이 필요합니다. Mosh는 UTF-8만 지원하도록 처음부터 설계됐습니다. 서버의 로케일 설정이 UTF-8이 아니면 실행을 거부해요. 한글을 쓰는 환경이라면 어차피 UTF-8일 테니 문제될 일은 드뭅니다.

tmux, Zellij와 함께 쓰기

Mosh의 스크롤백 문제와 멀티플렉서의 세션 관리는 서로를 잘 보완합니다. 접속하자마자 멀티플렉서 세션에 붙도록 해두면 편해요.

mosh user@example.com -- tmux new -A -s main

tmux new -A -s mainmain 세션이 있으면 붙고 없으면 만드는 명령입니다. 스크롤백은 tmux가 관리하니 지나간 출력을 얼마든지 되짚어 볼 수 있고, 혹시 Mosh 세션까지 완전히 끊기더라도 서버 쪽 tmux 세션은 살아 있어 다시 붙으면 그만입니다.

Zellij를 쓴다면 이렇게 하면 됩니다.

mosh user@example.com -- zellij attach --create main

즉 Mosh는 연결이 끊기지 않게 하고, 멀티플렉서는 끊기더라도 작업이 남아 있게 합니다. 두 겹으로 막아두는 셈이죠.

마치며

원격 서버에서 보내는 시간이 많다면 Mosh는 체감 효과가 큰 도구입니다. 노트북을 덮었다 열었을 때 세션이 그대로 살아 있는 경험을 한 번 하고 나면 되돌아가기 어려워요. 😄

정리하면 Mosh는 인증을 SSH에 맡기고, UDP 위에서 화면 상태를 동기화해 로밍과 즉각적인 키 입력 반응을 얻습니다. 대신 스크롤백과 포워딩 기능을 포기했으니, 포트 포워딩이 필요한 작업은 SSH로 하고 평소 원격 작업은 Mosh로 하는 식으로 나눠 쓰는 게 현실적입니다.

터미널 환경 자체를 손보고 싶어지셨다면 터미널 관련 글들도 함께 살펴보세요. 더 깊은 내용은 Mosh 공식 사이트SSP를 설명한 USENIX 논문에 잘 정리돼 있습니다.

This work is licensed under CC BY 4.0CCBY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord