Lompat ke konten
dokumentasi.id
Commit 0da916f
v1
Star0

Kamu membaca versi lama. Halaman ini hanya bisa dibaca.

6

Siklus Hidup Koneksi SSH

Satu koneksi SSH di Termimus dipegang oleh session_id yang sama dari awal sampai akhir. Backend menyimpan SshSession — berisi handle russh, cmd_tx untuk perintah tulis/resize, dan _jump_handle opsional — di dalam SessionManager src-tauri/src/ssh/mod.rs:216-221. SessionManager sendiri memelihara tiga struktur: sessions sebagai map sesi hidup, connecting sebagai penanda koneksi yang sedang berjalan, dan cancelled sebagai penanda sesi yang tabnya sudah ditutup sebelum koneksi selesai src-tauri/src/ssh/mod.rs:223-230.

Alur masuknya lewat command ssh_connect. Command itu memuat host dari database, menyusun SshAuth lewat resolve_host_auth, dan bila jump_host_id terisi juga memuat konfigurasi bastion menjadi JumpHostConfig src-tauri/src/commands/mod.rs:556-611. Setelah SessionManager::connect selesai, touch_host_last_connected memperbarui waktu koneksi terakhir host src-tauri/src/commands/mod.rs:556-611.

Peta langkah handshake

do_connect mengirim progres ke frontend pada setiap tahap melalui event ssh-progress-{session_id}. Payload-nya adalah SshProgressPayload dengan field step (1 sampai 5), step_name, message, timestamp, dan is_error src-tauri/src/ssh/mod.rs:37-45. Closure emit_progress di do_connect membungkus app.emit dengan nama event tersebut src-tauri/src/ssh/mod.rs:334-346.

stateDiagram-v2
    [*] --> connecting
    connecting --> host_key: "client::connect_stream"
    host_key --> authenticating: "check_server_key ok"
    authenticating --> opening_channel: "authenticate_publickey_smart"
    opening_channel --> ready: "request_pty + request_shell"
    ready --> closed: "Eof / Close / disconnect"
    connecting --> error: "Connection failed"
    authenticating --> error: "Authentication rejected by server"
    opening_channel --> error: "PTY request failed"
    error --> [*]
    closed --> [*]
    classDef ok fill:#2d7d46,color:#fff
    classDef warn fill:#f59e0b,color:#000
    classDef bad fill:#dc2626,color:#fff
    class ready,closed ok
    class host_key,authenticating warn
    class error bad
%% fig: Mesin keadaan handshake SSH
%% desc: Empat tahap handshake berjalan berurutan sebelum sesi siap, dan setiap kegagalan langsung melompat ke keadaan error.
Fig. 1: Mesin keadaan handshake SSHEmpat tahap handshake berjalan berurutan sebelum sesi siap, dan setiap kegagalan langsung melompat ke keadaan error.

Tabel berikut memetakan nilai yang benar-benar di-emit ke aksi backend-nya.

stepstep_nameAksi backendPesan yang di-emit
1connectingTcpStream::connect langsung, atau tunnel lewat jump hostResolving and connecting to {address}:{port}...
2host_keycheck_server_key pada SshClientHandlerVerifying target host key (Trust On First Use)...
3authenticatingauthenticate_password atau authenticate_publickey_smartAuthenticating as {username}...
4opening_channelchannel_open_session, request_pty, request_shellOpening shell channel...
5readysesi didaftarkan ke map sessionsShell session ready.

Langkah 1 sampai 4 diberi label progres masing-masing di src-tauri/src/ssh/mod.rs:462-483, src-tauri/src/ssh/mod.rs:521-547, dan langkah ready di src-tauri/src/ssh/mod.rs:553-567. Bila autentikasi ditolak, backend mengembalikan Authentication rejected by server src-tauri/src/ssh/mod.rs:485-517; bila PTY atau shell gagal dibuka, pesannya PTY request failed atau Shell request failed src-tauri/src/ssh/mod.rs:521-547.

Untuk kunci privat, authenticate_publickey_smart menegosiasikan algoritma RSA terkuat yang diterima server lewat best_supported_rsa_hash, lalu mencoba Sha256, Sha512, dan akhiran tanpa hash sebagai fallback src-tauri/src/ssh/mod.rs:177-214. Verifikasi kunci host beserta penolakan REMOTE HOST IDENTIFICATION HAS CHANGED dibahas di Verifikasi Host Key TOFU.

Transport dan keepalive

Sebelum handshake, stream yang sudah terbentuk dikonfigurasi oleh configure_tcp_stream: set_nodelay(true) mematikan Nagle, dan socket2::TcpKeepalive dipasang dengan with_time 15 detik serta with_interval 5 detik src-tauri/src/ssh/mod.rs:18-24. Di lapisan SSH, default_client_config mengisi keepalive_interval 15 detik dan keepalive_max 4 pada client::Config src-tauri/src/ssh/mod.rs:30-35. Keduanya mencegah router NAT stateful dan firewall VPC memutus koneksi yang menganggur.

graph LR
  tcp["TcpStream"] --> nodelay["set_nodelay(true)"]
  nodelay --> ka["TcpKeepalive 15s / 5s"]
  ka --> cfg["client::Config keepalive_interval 15s"]
  cfg --> srv["SSH server"]
  cfg -.->|"[email protected]"| srv
  style tcp fill:#336791,color:#fff
  style nodelay fill:#f59e0b,color:#000
  style ka fill:#f59e0b,color:#000
  style cfg fill:#2d7d46,color:#fff
  style srv fill:#8b5cf6,color:#fff
%% fig: Dua lapis keepalive
%% desc: Keepalive dipasang pada socket TCP dan pada protokol SSH agar koneksi menganggur tidak diputus middlebox.
Fig. 2: Dua lapis keepaliveKeepalive dipasang pada socket TCP dan pada protokol SSH agar koneksi menganggur tidak diputus middlebox.

Tanpa jump host, stream berupa SshStream::Direct hasil tokio::net::TcpStream::connect src-tauri/src/ssh/mod.rs:350-364. Dengan jump host, Termimus membuka kanal channel_open_direct_tcpip di koneksi bastion dan membungkusnya menjadi SshStream::Tunneled src-tauri/src/ssh/mod.rs:426-442. Detail aturan tunnel ada di Port Forwarding dan Tunnel.

Worker PTY dan koalesensi ~8ms

Setelah ready, do_connect membuat mpsc::unbounded_channel::<SessionCommand>() dan mendaftarkan sesi ke map sessions src-tauri/src/ssh/mod.rs:553-567. Satu task tokio::spawn kemudian menjadi worker yang membaca channel.wait() dan mengirim byte server ke event ssh-data-{session_id} src-tauri/src/ssh/mod.rs:575-611.

graph LR
  ch["channel.wait()"] --> idle{"flush_deadline"}
  idle -->|"None"| direct["emit ssh-data immediately"]
  idle -->|"Some"| buf["pending.extend_from_slice"]
  buf --> cap{"pending reached 64KB"}
  cap -->|"yes"| flush["emit ssh-data, flush_deadline = None"]
  cap -->|"no"| timer["sleep_until deadline 8ms"]
  timer --> flush
  cmd["cmd_rx.recv()"] --> write["channel.data"]
  cmd --> resize["window_change"]
  style direct fill:#2d7d46,color:#fff
  style buf fill:#f59e0b,color:#000
  style flush fill:#2d7d46,color:#fff
  style timer fill:#8b5cf6,color:#fff
  style resize fill:#dc2626,color:#fff
%% fig: Koalesensi output PTY di worker
%% desc: Output saat idle dikirim seketika, sedangkan burst dikumpulkan sampai 8ms atau 64KB sebelum di-emit; perintah resize di bawah ambang diabaikan.
Fig. 3: Koalesensi output PTY di workerOutput saat idle dikirim seketika, sedangkan burst dikumpulkan sampai 8ms atau 64KB sebelum di-emit; perintah resize di bawah ambang diabaikan.

Aturan koalesensinya eksplisit di kode. Saat flush_deadline masih None (stream idle, misalnya gema ketikan), byte langsung di-emit dengan jeda 0ms dan timer flush_delay 8ms dimulai. Selama timer itu hidup, byte berikutnya diakumulasi ke pending dan baru dikirim saat pending.len() >= 64 * 1024 atau saat timer jatuh tempo src-tauri/src/ssh/mod.rs:575-611. Cabang timer mem-flush sisa pending lalu mengembalikan flush_deadline ke None, dan cabang Eof, Close, atau None mem-flush sisa byte sebelum meng-emit ssh-closed-{session_id} src-tauri/src/ssh/mod.rs:629-644.

Sisi perintah memakai SessionCommand. Varian Data diteruskan ke channel.data, sedangkan varian Resize hanya memanggil channel.window_change bila cols >= 15 && rows >= 4 src-tauri/src/ssh/mod.rs:645-658.

⚠️ Resize dengan cols di bawah 15 atau rows di bawah 4 dibuang diam-diam oleh worker. Ukuran itu muncul saat elemen DOM terminal disembunyikan, sehingga mengirimkannya akan memperkecil PTY remote secara keliru src-tauri/src/ssh/mod.rs:645-658.

SessionManager::write dan SessionManager::resize tidak menyentuh kanal secara langsung; keduanya hanya mengirim SessionCommand lewat cmd_tx, dan mengembalikan Session not found bila session_id tidak ada di map src-tauri/src/ssh/mod.rs:698-721.

Pembatalan dan penutupan

Frontend memulai semuanya lewat startConnection, yang memanggil api.connectSsh(hostId, sessionId, cols, rows) dan menandai tab connected saat promise selesai src/components/terminal/XtermView.tsx:464-517. Tiga listener dipasang per sesi: ssh-data-{sessionId} menulis byte ke term, ssh-closed-{sessionId} memanggil closeSession, dan ssh-progress-{sessionId} menambahkan ConnectionLog sekaligus menyimpan connectionStep serta connectionError ke terminalPool agar state bertahan saat pane di-remount src/components/terminal/XtermView.tsx:657-705. Bentuk payload yang dibaca frontend adalah step, step_name, message, timestamp, is_error src/components/terminal/XtermView.tsx:40-46.

closeSession melakukan dua hal berurutan: disposeTerminalSession(sessionId) untuk melepas listener dan instance xterm src/components/terminal/XtermView.tsx:312-352, lalu api.disconnectSsh(sessionId) src/stores/useSessionStore.ts:524-542. Command ssh_disconnect hanya meneruskan panggilan ke SessionManager::disconnect src-tauri/src/commands/mod.rs:632-638.

disconnect menangani dua keadaan. Bila sesi sudah terdaftar, ia mengeluarkannya dari map lalu memanggil handle.disconnect(Disconnect::ByApplication, "", "en") beserta koneksi bastion bila ada. Bila sesi belum terdaftar tetapi id-nya ada di connecting, id itu dimasukkan ke cancelled src-tauri/src/ssh/mod.rs:723-743. Penanda cancelled dibaca check_cancelled di setiap titik henti do_connect, yang mengembalikan Connection cancelled src-tauri/src/ssh/mod.rs:310-316. connect juga menolak dua percobaan bersamaan untuk satu session_id dengan pesan Connection already in progress for this session, dan membersihkan sesi lama ber-id sama sebelum mencoba lagi src-tauri/src/ssh/mod.rs:255-280.

Saat aplikasi keluar, RunEvent::ExitRequested memanggil disconnect_all di dalam tokio::time::timeout 2 detik agar server menerima pesan disconnect yang benar, bukan socket TCP yang hilang src-tauri/src/lib.rs:118-132. disconnect_all menyalin daftar id lalu memanggil disconnect untuk masing-masing src-tauri/src/ssh/mod.rs:748-756. Instance SessionManager sendiri dibuat sekali di run() dan dibagi lewat AppState src-tauri/src/lib.rs:25.

Event dan penanganannya

EventPayloadPenanganan
ssh-data-{session_id}byte PTY sebagai Vec<u8>term.write(bytes) di XtermView
ssh-progress-{session_id}SshProgressPayloadlog koneksi, connectionStep, connectionError
ssh-closed-{session_id}session_idcloseSession(sessionId)

Ketiga nama event dibentuk dari session_id di src-tauri/src/ssh/mod.rs:569-570 dan src-tauri/src/ssh/mod.rs:334-346, lalu didengarkan di src/components/terminal/XtermView.tsx:657-705. Setelah ready, backend juga menjalankan deteksi OS di latar belakang lewat detect_os_via_exec pada kanal exec terpisah, dimulai setelah jeda 800ms dan dilewati bila koneksi melewati jump host src-tauri/src/ssh/mod.rs:663-693. Kanvas terminal, split pane, dan perilaku fokusnya dibahas di Terminal, Split Pane, dan Workspace, sedangkan kanal SFTP yang memakai kredensial host yang sama ada di SFTP Dual-Pane dan Editor Remote.