Lompat ke konten
dokumentasi.id
Commit 0da916f
v1
Star0

Kamu membaca versi lama. Halaman ini hanya bisa dibaca.

11

Port Forwarding dan Tunnel

Port forwarding di Termimus dijalankan oleh TunnelManager di src-tauri/src/tunnel/mod.rs:14-21. Manager menyimpan tunnel yang sedang hidup di dalam HashMap<String, ActiveTunnel> yang dibungkus Mutex, dan setiap entri membawa rule_id serta stop_tx bertipe oneshot::Sender<()>. Karena kuncinya adalah rule_id, satu rule hanya boleh punya satu listener aktif. Dua method baca, is_active dan list_active, hanya membaca kunci HashMap itu tanpa menyentuh jaringan src-tauri/src/tunnel/mod.rs:30-36.

Siklus hidup satu tunnel

start_local_forward adalah satu-satunya pintu masuk untuk menyalakan tunnel. Urutannya: cek duplikat, buka koneksi SSH, autentikasi, bind listener lokal, spawn task penerima koneksi, lalu daftarkan rule ke HashMap.

Pemeriksaan duplikat dilakukan sebelum menyentuh jaringan. Jika rule_id sudah ada di active, fungsi langsung mengembalikan "Tunnel is already running" src-tauri/src/tunnel/mod.rs:55-60.

Setelah itu fungsi membangun SshClientHandler { address, port, db }, membuka TcpStream::connect ke host SSH, memanggil configure_tcp_stream, lalu menyerahkan socket itu ke client::connect_stream. Dua kegagalan di tahap ini diberi pesan berbeda: "Tunnel TCP connection failed: {e}" dan "Tunnel SSH connection failed: {e}" src-tauri/src/tunnel/mod.rs:62-77. Handler yang dipakai adalah handler yang sama dengan sesi terminal, sehingga verifikasi host key tetap berlaku; mekanismenya dijelaskan di Verifikasi Host Key TOFU.

Autentikasi bercabang pada SshAuth. Varian Password memakai authenticate_password; varian PrivateKey mendekode pem lewat decode_secret_key lalu memanggil authenticate_publickey_smart. Bila server menolak, hasilnya "Authentication rejected by server" src-tauri/src/tunnel/mod.rs:79-93.

Listener lokal baru dibuat setelah autentikasi berhasil. Alamat bind dibentuk dari format!("{local_address}:{local_port}") dan kegagalan bind dilaporkan sebagai "Failed to bind {bind_addr}: {e}" src-tauri/src/tunnel/mod.rs:95-99. Ini penting: tunnel yang gagal autentikasi tidak pernah meninggalkan port lokal yang terbuka.

Meneruskan satu koneksi

Task hasil tokio::spawn menjalankan tokio::select! antara stop_rx dan listener.accept(). Setiap koneksi yang diterima di-spawn lagi ke forward_connection dengan originator_ip dan originator_port dari peer_addr, sehingga satu koneksi lambat tidak memblokir koneksi lain. Task mengirim event tunnel-status-{rule_id} bernilai "started" di awal dan "stopped" saat loop berhenti; kegagalan per koneksi dicatat sebagai "Tunnel forwarding error: {e}" dan kegagalan accept sebagai "Tunnel accept error: {e}" src-tauri/src/tunnel/mod.rs:106-142. Setelah task di-spawn, barulah rule dimasukkan ke active src-tauri/src/tunnel/mod.rs:144-147.

Diagram berikut menunjukkan perjalanan satu paket dari listener lokal sampai ke remote_address:remote_port.

sequenceDiagram
    autonumber
    participant App as Local client
    participant L as TcpListener local_address:local_port
    participant F as forward_connection
    participant C as direct-tcpip channel
    participant R as remote_address:remote_port

    App->>L: TCP connect
    L->>F: accept() -> (stream, peer_addr)
    F->>C: channel_open_direct_tcpip(remote_address, remote_port, originator_ip, originator_port)
    App->>C: stream.read(buf) -> channel.data(buf)
    R-->>C: ChannelMsg::Data
    C-->>App: stream.write_all(data)
    App->>C: read 0 byte -> channel.eof()
    C-->>F: ChannelMsg::Eof / Close / None

%% fig: Perjalanan paket dari listener lokal ke remote target %% desc: Setiap koneksi TCP lokal menjadi satu channel direct-tcpip, lalu data dipompa dua arah sampai salah satu sisi menutup channel.

forward_connection mengaktifkan set_nodelay(true) pada socket lokal, lalu membuka channel lewat channel_open_direct_tcpip(remote_address, remote_port as u32, originator_ip, originator_port); kegagalannya berbunyi "Failed to open direct-tcpip channel: {e}" src-tauri/src/tunnel/mod.rs:159-171. Perhatikan bahwa remote_address dikirim apa adanya ke server SSH, jadi tujuan akhirnya adalah jaringan yang terlihat dari sisi server, bukan dari mesin lokal.

Pemompaan data memakai buffer vec![0u8; 32 * 1024] dan satu tokio::select! dua arah. Arah lokal ke remote: Ok(0) menandai EOF dan memicu channel.eof(), sedangkan Ok(n) mengirim channel.data(&buf[..n]). Arah remote ke lokal: ChannelMsg::Data ditulis lewat stream.write_all, dan ChannelMsg::Eof, ChannelMsg::Close, atau None mengakhiri loop src-tauri/src/tunnel/mod.rs:173-208.

Mematikan tunnel

stop menghapus entri dari HashMap lalu mengirim sinyal lewat stop_tx.send(()). Pengiriman sinyal itu yang membuat tokio::select! di task penerima keluar dari loop, menutup listener, dan mengirim event "stopped" src-tauri/src/tunnel/mod.rs:150-156. Karena entri dihapus lebih dulu, stop yang dipanggil dua kali tidak menghasilkan error: ia hanya tidak menemukan apa pun untuk dihentikan.

Diagram berikut memisahkan jalur kendali dari frontend dan tempat status sebenarnya disimpan.

graph LR
    Card["TunnelCard toggle"] --> Store["useTunnelStore.toggleTunnel"]
    Store --> Invoke["api.startTunnel / api.stopTunnel"]
    Invoke --> Cmd["tunnel_start / tunnel_stop"]
    Cmd --> Rule["port_forwards row"]
    Cmd --> Registry["TunnelManager.active HashMap"]
    Registry --> Task["tokio task + TcpListener"]
    Task --> Event["event tunnel-status-rule_id"]
    Store --> List["api.listActiveTunnels"]
    List --> Registry

    style Registry fill:#336791,color:#fff
    style Rule fill:#336791,color:#fff
    style Task fill:#2d7d46,color:#fff
    style Event fill:#8b5cf6,color:#fff
    style Cmd fill:#f59e0b,color:#000

%% fig: Jalur kendali tunnel dari UI ke registry %% desc: Frontend memanggil command Tauri, command membaca rule dari SQLite lalu mengubah isi HashMap TunnelManager, dan status aktif dibaca kembali lewat tunnel_active_list.

Command dan jembatan ke frontend

Lima command Tauri menjadi permukaan API fitur ini. tunnel_rule_list, tunnel_rule_save, dan tunnel_rule_delete hanya menyentuh database; ketiganya tidak pernah menyalakan listener src-tauri/src/commands/mod.rs:1190-1228. tunnel_start adalah satu-satunya yang menyentuh jaringan: ia membaca rule lewat get_port_forward, membaca host lewat get_host, menyelesaikan kredensial lewat resolve_host_auth, lalu memanggil start_local_forward src-tauri/src/commands/mod.rs:1230-1266. Dua kegagalan lookup punya pesan sendiri, "Port forwarding rule not found" dan "Target host not found". tunnel_stop dan tunnel_active_list hanya meneruskan ke stop dan list_active src-tauri/src/commands/mod.rs:1268-1281.

Di sisi TypeScript, keenam pemanggilan dibungkus sebagai listTunnelRules, saveTunnelRule, deleteTunnelRule, startTunnel, stopTunnel, dan listActiveTunnels src/lib/api.ts:328-335.

Command TauriMembaca/menulisMenyentuh jaringan
tunnel_rule_listlist_port_forwardstidak
tunnel_rule_saveget_port_forward, save_port_forwardtidak
tunnel_rule_deletedelete_port_forward + tombstonetidak
tunnel_startget_port_forward, get_hostya, lewat start_local_forward
tunnel_stopTunnelManager.activemenutup listener
tunnel_active_listTunnelManager.activetidak

Store dan tampilan

useTunnelStore menyimpan rules dan activeRuleIds sebagai Set<string>. refresh memanggil api.listTunnelRules() dan api.listActiveTunnels() secara paralel dengan Promise.all, lalu menyusun ulang activeRuleIds dari daftar id yang dikembalikan backend. Jadi status aktif di UI selalu berasal dari TunnelManager, bukan dari state lokal optimistis src/stores/useTunnelStore.ts:31-44.

toggleTunnel memilih arah berdasarkan keanggotaan id di activeRuleIds, menyetel startingRuleId selama operasi berjalan, dan menulis error berbentuk Tunnel error: ${String(e)} bila gagal. deleteRule mematikan tunnel lebih dulu jika rule sedang aktif, baru menghapus barisnya src/stores/useTunnelStore.ts:51-77. Di TunnelCard, indikator titik hijau berdenyut dan tombol Stop Tunnel hanya muncul ketika isActive bernilai benar; tombol Edit dan Delete dinonaktifkan selama tunnel aktif src/components/tunnels/TunnelView.tsx:339-357. Kartu juga menampilkan pasangan local_address:local_port berlabel Local: dan remote_address:remote_port berlabel Target:, plus host perantara sebagai via <label> src/components/tunnels/TunnelView.tsx:382-404.

Tabel berikut merangkum field yang harus diisi saat membuat rule.

FieldNilai awal di TunnelModalWajib
labelkosongya, pesan "Label is required"
host_idhost pertamaya, pesan "Please select an SSH host"
forward_type"local"tidak divalidasi
local_address"127.0.0.1"tidak, jatuh ke "127.0.0.1"
local_port8080ya
remote_address"127.0.0.1"tidak, jatuh ke "127.0.0.1"
remote_port80ya

Saat submit, TunnelModal merakit PortForwardInput dan memanggil saveRule(input, editingRule?.id); local_address dan remote_address yang kosong diganti "127.0.0.1" sebelum dikirim src/components/tunnels/TunnelModal.tsx:45-66. Nilai awalnya sendiri ditetapkan di state komponen src/components/tunnels/TunnelModal.tsx:12-20.

Penyimpanan rule

Rule disimpan di tabel port_forwards dengan id sebagai primary key dan foreign key host_id ke hosts(id) yang memakai ON DELETE CASCADE; menghapus host otomatis menghapus rule-nya src-tauri/src/db/mod.rs:79-90. Skema lengkap dan migration-nya ada di Skema Database Lokal.

KolomTipeDefault
idTEXTprimary key
host_idTEXTtidak ada, NOT NULL
labelTEXTtidak ada, NOT NULL
forward_typeTEXT'local'
local_addressTEXT'127.0.0.1'
local_portINTEGERtidak ada, NOT NULL
remote_addressTEXT'127.0.0.1'
remote_portINTEGERtidak ada, NOT NULL
created_atTEXTtidak ada, NOT NULL

save_port_forward memakai INSERT ... ON CONFLICT(id) DO UPDATE, jadi menyimpan rule dengan id yang sama memperbarui kolom selain created_at src-tauri/src/db/mod.rs:635-661. delete_port_forward menghapus baris lalu menulis tombstone bertipe 'port_forward', sehingga penghapusan ikut terbawa saat sinkronisasi src-tauri/src/db/mod.rs:663-673. Perilaku tombstone dibahas di Backup, Sync Relay, dan API Server.

⚠️ forward_type tersimpan dan divalidasi sebagai nilai local, remote, atau dynamic src-tauri/src/db/models.rs:164-188, tetapi jalur eksekusi hanya membuka satu jenis tunnel. tunnel_start memanggil start_local_forward tanpa membaca rule.forward_type, dan start_local_forward selalu melakukan TcpListener::bind pada local_address:local_port src-tauri/src/commands/mod.rs:1230-1266. Rule bertipe remote atau dynamic tetap tersimpan, tetapi tidak mengubah arah penerusan.

⚠️ local_address menentukan antarmuka bind. Nilai bawaan 127.0.0.1 hanya menerima koneksi dari mesin sendiri, sedangkan nilai seperti 0.0.0.0 membuka port itu ke seluruh jaringan, dan TcpListener::bind tidak memeriksa siapa pun yang datang src-tauri/src/tunnel/mod.rs:95-99. Periksa kembali field Bind IP sebelum menyalakan tunnel di mesin yang terhubung ke jaringan bersama.

PortForwardRule yang dikirim ke frontend membawa sembilan field: id, host_id, label, forward_type, local_address, local_port, remote_address, remote_port, dan created_at; PortForwardInput adalah versi tanpa id dan created_at src-tauri/src/db/models.rs:190-212.