remote.เอกสารหน้าหลัก

โปรโตคอลการสื่อสาร

เอกสารนี้ระบุรูปแบบการโยงที่สมบูรณ์ระหว่าง agent, exchange และ cli คำจำกัดความอยู่ใน crates/proto และทั้งสามใช้รหัสเดียวกัน: ฟิลด์ถูกกำหนดไว้ในที่เดียว ดังนั้นความไม่สอดคล้องกันจึงปรากฏขึ้นในเวลาคอมไพล์ แทนที่จะเป็นในระหว่างเซสชันการใช้งานจริง

ให้บริการแก่ผู้ให้บริการตรวจสอบการรับส่งข้อมูลเครือข่ายเพื่อวินิจฉัยปัญหา และนักพัฒนาเปลี่ยนโปรโตคอลหรือเขียนไคลเอนต์บุคคลที่สาม


สารบัญ


ภาพรวม

โปรโตคอลมีสองชั้นที่ใช้การเชื่อมต่อ WebSocket ร่วมกัน:

เลเยอร์ ผู้ให้บริการ การเข้ารหัส สารบัญ
ควบคุมข้อความ WebSocket ข้อความ เฟรม เจสัน การลงทะเบียน ฮาร์ทบีท ช่วงเปิด/ปิด การแจ้งเตือนผล
กรอบข้อมูล WebSocket binary เฟรม ส่วนหัวคงที่ 9 ไบต์ + ไบต์ดิบ สตรีมไบต์ PTY คำสั่ง stdout/stderr

การฝังข้อมูลสตรีมมิ่งใน JSON เป็น base64 จะเพิ่มขนาดขึ้นประมาณ 33% การรับส่งข้อมูล PTY ประกอบด้วยแพ็กเก็ตขนาดเล็กบ่อยครั้งซึ่งสะท้อนการกดแป้นพิมพ์แต่ละครั้ง ดังนั้นโอเวอร์เฮดนี้จึงส่งผลโดยตรงต่อเวลาแฝงของการโต้ตอบ ข้อความควบคุมไม่บ่อยนัก ทำให้การอ่าน JSON มีคุณค่ามากขึ้น: การจับแพ็กเก็ตสามารถเข้าใจได้ทันที

ตามแบบแผน แต่ละเฟรมไบนารี WebSocket ** จะมีหนึ่งเฟรมข้อมูล** เท่านั้น ไบต์ที่เกินหรือหายไปถือเป็นข้อผิดพลาด


การขนส่งและจุดสิ้นสุด

จุดสิ้นสุด ลูกค้า การรับรองความถูกต้อง
GET /ws/device agent เฟรมแรก หลังจากสร้างการเชื่อมต่อต้องเป็นข้อความควบคุม register ที่มี device_id และ token
GET /ws/operator remote-cli ส่วนหัวคำขอ HTTP Authorization: Bearer <运维令牌> ในการจับมือ WebSocket

ทั้งสองเส้นทางใช้ประเภทข้อมูลรับรองแยกกัน โทเค็นอุปกรณ์ไม่สามารถเข้าถึงจุดสิ้นสุดของผู้ให้บริการ และในทางกลับกัน

ข้อมูลประจำตัวของผู้ให้บริการใช้ส่วนหัวของคำขอแทนที่จะเป็นสตริงการสืบค้น URL เนื่องจาก URL ปรากฏในบันทึกพร็อกซีย้อนกลับ ประวัติเบราว์เซอร์ และส่วนหัวของผู้อ้างอิง โทเค็นอุปกรณ์ใช้เฟรมแรกแทนที่จะเป็นส่วนหัวคำขอ เนื่องจากไคลเอ็นต์แบบฝังบางตัวไม่สามารถปรับแต่งส่วนหัว handshake ของ WebSocket ได้อย่างง่ายดาย วิธีเฟรมแรกยังอนุญาตให้รุ่นโปรโตคอลไม่ตรงกันและโทเค็นที่ไม่ถูกต้องเพื่อใช้ข้อความตอบกลับเดียวกัน

การใช้งานจริงต้องใช้ wss://: ข้อมูลประจำตัวทั้งสองประเภทจะปรากฏเป็นข้อความธรรมดาระหว่างการจับมือกัน


เวอร์ชันโปรโตคอล

pub const PROTOCOL_VERSION: u16 = 1;

มีการเปรียบเทียบเวอร์ชันทั้งสองทิศทางระหว่างการจับมือกัน ความไม่ตรงกันทำให้เกิดการปฏิเสธทันทีพร้อมคำอธิบายที่ชัดเจน วิธีนี้จะป้องกันไม่ให้เวอร์ชันที่ไม่ตรงกันปรากฏเป็นความล้มเหลวในการแยกวิเคราะห์ที่อธิบายไม่ได้ในข้อความในภายหลัง ซึ่งอาจเป็นเรื่องยากที่จะวินิจฉัย

  • ฝั่งอุปกรณ์: agent มีเวอร์ชันอยู่ใน register ในกรณีที่ไม่ตรงกัน เซิร์ฟเวอร์ส่งคืน register_ack ด้วย ok: false อธิบายทั้งสองเวอร์ชันใน error และปิดการเชื่อมต่อ
  • ฝั่งผู้ดำเนินการ: เซิร์ฟเวอร์จะส่ง welcome ในเชิงรุกหลังจากสร้างการเชื่อมต่อ หาก CLI ตรวจพบความไม่ตรงกัน ระบบจะรายงานข้อผิดพลาดและแนะนำให้ผู้ใช้อัปเกรดเป็นเวอร์ชันที่ตรงกับ server_version

ข้อความที่ไม่รู้จัก ค่า type จะถูกปฏิเสธเสมอ ไม่เคยตีความว่าเป็นประเภทที่รู้จักโดยไม่โต้ตอบ


ควบคุมข้อความ

ข้อความควบคุมทั้งหมดเป็นอ็อบเจ็กต์ JSON ที่แท็กโดยฟิลด์ type (serde enum ที่แท็กภายใน) ค่าแท็กและชื่อฟิลด์ใช้ snake_case session_id ได้รับการซีเรียลไลซ์อย่างโปร่งใสเป็นตัวเลขธรรมดา

อุปกรณ์ขึ้น (agent → exchange)

type เขตข้อมูล วัตถุประสงค์
register protocol_version: u16
device_id: string
token: string
info: DeviceInfo
ต้องเป็นข้อความ first หลังการเชื่อมต่อ เซิร์ฟเวอร์ไม่ยอมรับข้อความอื่นก่อนหน้านั้น โดยมีกำหนดเวลา 15 วินาที
heartbeat metrics: Metrics ตัวชี้วัดการดำเนินงานเป็นระยะ ความสดของการเชื่อมต่อได้รับการจัดการแยกกันโดยปิงปอง/ปิงปอง WebSocket ดั้งเดิม: ฮาร์ตบีทที่หายไปไม่ได้หมายความถึงการเชื่อมต่อที่ขาด หรือในทางกลับกัน
exec_started session_id
job_id
hold
timeout_secs
คำสั่งเริ่มต้น; job_id ใช้ในรายการพักและสำหรับการติดตั้งใหม่หลังจากเชื่อมต่อใหม่
exec_exit session_id
code: i32 | null
timed_out: bool
คำสั่งเสร็จสิ้น code คือ null เมื่อสัญญาณยุติกระบวนการ timed_out บ่งชี้ถึงการบังคับยุติเนื่องจากการหมดเวลา
exec_list_resp session_id
jobs: ExecHoldInfo[]
รายการงานค้าง
ubus_resp session_id
code: i32
data: any
ผลการโทร ubus code คือโค้ดส่งคืน ubus 0 หมายถึงความสำเร็จ
android_resp session_id
result: AndroidResult
การทำงานของหน้าจอ Android เสร็จสมบูรณ์ ข้อความนี้จะสิ้นสุดเซสชัน
pty_closed session_id
code: i32 | null
เซสชัน PTY สิ้นสุดแล้ว code คือโค้ดทางออกของเชลล์
session_error session_id
message: string
ข้อผิดพลาดระดับเซสชัน อุปกรณ์จะส่งสิ่งนี้เมื่อไม่สามารถดำเนินการตามคำขอได้ แทนที่จะส่งไปโดยไม่ตั้งใจ

register ตัวที่สองบนการเชื่อมต่อที่ลงทะเบียนไว้แล้วจะถือเป็นการใช้โปรโตคอลในทางที่ผิดและบันทึกเป็นคำเตือน

ตัวอย่าง:

{"type":"register","protocol_version":1,"device_id":"lab-router","token":"a1b2….c2Vj…","info":{"hostname":"OpenWrt","model":"ath79/generic","openwrt_version":"OpenWrt 23.05.5","kernel":"5.15.137","arch":"mips","agent_version":"0.1.0"}}
{"type":"exec_exit","session_id":31,"code":0,"timed_out":false}

อุปกรณ์ลง (exchange → agent)

type เขตข้อมูล วัตถุประสงค์
register_ack ok: bool
error: string | null
heartbeat_interval_secs: u64
ผลการลงทะเบียน ด้วย ok: false นั้น error จะจัดเตรียมเหตุผล จากนั้นเซิร์ฟเวอร์จะปิดการเชื่อมต่อ เซิร์ฟเวอร์ระบุช่วงเวลาการเต้นของหัวใจเพื่อให้สามารถลดความถี่จากส่วนกลางได้เมื่อฟลีตเติบโตขึ้นโดยไม่ต้องกำหนดค่าอุปกรณ์แต่ละเครื่องใหม่ agent หนีบ 0 ถึงอย่างน้อย 1 วินาทีเพื่อป้องกันไม่ให้ตัวจับเวลาไม่ว่างทำให้ CPU อิ่มตัว
device_access managers: DeviceManager[]
updated_at: u64 (มิลลิวินาที)
ใครได้รับอนุญาตในอุปกรณ์นี้ DeviceManager = operator_id, username, email?, role, level (manage/view), source (role_default/grant) ส่งหลังจากการลงทะเบียนและการอนุญาตเปลี่ยนแปลงเฉพาะกับตัวแทนที่โฆษณาความสามารถ device_access เท่านั้น ดูที่เก็บ docs/permissions.md
exec_req session_id
command: string
timeout_secs: u64
hold: bool (ค่าเริ่มต้น true)
hold_timeout_secs?: u64
ดำเนินการคำสั่ง ด้วย hold=true (ค่าเริ่มต้น) กระบวนการสามารถดำเนินต่อไปได้หลังจากที่ตัวดำเนินการยกเลิกการเชื่อมต่อ การหมดเวลาใช้ hold_timeout_secs (ขาด/0 → 3600, ฮาร์ดสูงสุด 86400) hold=false ยกเลิกคำสั่งเมื่อตัวดำเนินการยกเลิกการเชื่อมต่อ
exec_cancel session_id ยุติการดำเนินการคำสั่ง (ฆ่า) ก่อนกำหนด
exec_detach session_id หยุดการสตรีมเอาต์พุตไปยังตัวดำเนินการปัจจุบันในขณะที่ยังคงกระบวนการพักไว้
exec_list_req session_id สอบถามงานระงับปัจจุบัน
exec_attach_req session_id
job_id
แนบงานการระงับเข้ากับเซสชันนี้อีกครั้ง และเล่นซ้ำเอาต์พุตที่บัฟเฟอร์
exec_hold_kill session_id
job_id
ยุติงานการระงับโดย job_id
exec_hold_tail session_id
job_id
ติดตามเอาต์พุตใหม่เท่านั้น โดยไม่ต้องเล่นบัฟเฟอร์ซ้ำ
exec_hold_cat session_id
job_id
ดัมพ์บัฟเฟอร์ปัจจุบันและจบด้วย exec_hold_cat_done
ubus_req session_id
object: string
method: string
params: any
โทรออกรถอูบัส เมื่อ params คือ null จะไม่มีการส่งผ่านอาร์กิวเมนต์ตำแหน่งที่สามไปยัง ubus
pty_open session_id
rows: u16
cols: u16
เปิดเซสชัน PTY ขนาดศูนย์ถอยกลับไปที่ 24×80
pty_resize session_id
rows: u16
cols: u16
ขนาดของหน้าต่างเทอร์มินัลมีการเปลี่ยนแปลง
pty_close session_id ปิดเซสชัน PTY
android_req session_id
request: AndroidAction
การทำงานของหน้าจอ Android หนึ่งหน้าจอ
android_cancel session_id ยกเลิกคำร้องขอ Android ที่ยังไม่เสร็จสิ้น

OperatorUp (cli → exchange)

การตรวจสอบความถูกต้องเกิดขึ้นระหว่างการจับมือ WebSocket ดังนั้นจึงไม่มีข้อความเข้าสู่ระบบ

type เขตข้อมูล วัตถุประสงค์
open_exec session_id
device_id
command
timeout_secs
hold (ค่าเริ่มต้นเป็นจริง)
hold_timeout_secs?
ดำเนินการคำสั่งบนอุปกรณ์เป้าหมาย การระงับถูกเปิดใช้งานตามค่าเริ่มต้น hold=true พร้อมระยะหมดเวลา 0 → 3600 วินาที hold=false พร้อมการหมดเวลา 0 → Exchange ค่าเริ่มต้น 60 วินาที
list_exec_holds session_id
device_id
ค้นหางานผู้บริหารและการดาวน์โหลด การอัพโหลด และการสืบค้นไดเร็กทอรีที่ใช้งานอยู่
list_device_sessions session_id
device_id
ค้นหาตารางเส้นทาง Exchange โดยตรงโดยไม่ต้องใช้สล็อตเซสชันของอุปกรณ์
kill_device_session session_id
device_id
target_session_id
ยกเลิกโดย target_session_id ฝั่งอุปกรณ์ และปล่อยเส้นทางในเครื่อง รวมการถือครอง exec และไม่ใช้ช่องพิเศษ
attach_exec session_id
device_id
job_id
แนบไปกับงานพักและรับเอาต์พุตต่อ
exec_hold_kill / exec_hold_tail / exec_hold_cat session_id
device_id
job_id
ฆ่า / หาง / แมว สำหรับงานค้าง
detach_session session_id หยุดการรอในเบื้องหน้าและแยกออกโดยไม่ทำให้กระบวนการระงับหยุดลง
open_ubus session_id
device_id: string
object: string
method: string
params: any
โทรออกรถอูบัส
open_pty session_id
device_id: string
rows: u16
cols: u16
เปิดเซสชัน PTY แบบโต้ตอบ
open_android session_id
device_id: string
request: AndroidAction
เริ่มต้นการทำงานของหน้าจอ Android ห้ามใช้บัญชีแบบอ่านอย่างเดียว
pty_resize session_id
rows: u16
cols: u16
ขนาดของหน้าต่างเทอร์มินัลมีการเปลี่ยนแปลง ไม่มี device_id: มีเซสชันอยู่แล้ว ดังนั้นเซิร์ฟเวอร์จึงสามารถค้นหาอุปกรณ์เป้าหมายได้
close_session session_id สิ้นสุดเซสชันในเชิงรุก: การออก PTY ปกติหรือการขัดจังหวะการดำเนินการคำสั่งของผู้ใช้

session_id ในข้อความ open_* ทั้งสี่ข้อความคือ ** จัดสรรโดย CLI เอง** (ดู Session ID Translation ระหว่างสอง Namespaces) อนุญาตให้ใช้เฟรมที่ตามมาทันทีหลังจากการร้องขอโดยไม่ต้องรอการตอบรับ ID

ตัวดำเนินการลง (exchange → cli)

type เขตข้อมูล วัตถุประสงค์
welcome protocol_version: u16
server_version: string
ส่งเชิงรุกโดยเซิร์ฟเวอร์หลังจากสร้างการเชื่อมต่อเพื่อตรวจสอบเวอร์ชันโปรโตคอลที่ตรงกัน
device_sessions session_id
sessions: DeviceSessionInfo[]
limit
count
เซสชันฝั่งอุปกรณ์ปัจจุบันใน Exchange
device_session_killed session_id
target_session_id
released: bool
เผยแพร่ในประเทศ เท็จถ้ามันไม่มีอยู่
exec_exit session_id
code: i32 | null
timed_out: bool
คำสั่งสิ้นสุดลง แปลจากข้อความฝั่งอุปกรณ์ที่มีชื่อเดียวกัน
ubus_resp session_id
code: i32
data: any
ผลการโทร ubus
android_resp session_id
result: AndroidResult
ผลลัพธ์ Android พร้อม ID เซสชันที่แปลเป็นเนมสเปซของตัวดำเนินการ
pty_closed session_id
code: i32 | null
เซสชัน PTY สิ้นสุดแล้ว
session_failed session_id
reason: SessionFailure
message: string
เซสชันล้มเหลว reason แยกสาเหตุเพื่อให้ CLI สามารถตัดสินใจได้ว่าการลองใหม่นั้นคุ้มค่าหรือไม่

โครงสร้างข้อมูลที่ใช้ร่วมกัน

ExecHoldInfo: job_id, kind (exec / download / upload / file), age_secs, session_id?, command_preview, status, timeout_secs, started_at_unix_ms, exit_code?, timed_out. เพื่อความเข้ากันได้ kind ที่หายไปจะมีค่าเริ่มต้นเป็น exec และไม่มี age_secs ที่มีค่าเริ่มต้นเป็น 0 รายการคำตอบจะคำนวณอายุจากนาฬิกาปัจจุบัน สรุปพาธคำสั่ง/ไฟล์จำกัดอยู่ที่ประมาณ 120 อักขระ งานที่ไม่ใช่ exec รองรับการ kill เท่านั้น ไม่ใช่ tail/cat/attach

DeviceSessionInfo: session_id: u32 (ID ฝั่งอุปกรณ์), operator_session_id: u32, kind: string, hold: bool, age_secs: u64, deadline_unix_ms: u64 Exchange ตอบกลับรายการเซสชันและคำขอการยกเลิกที่บังคับโดยตรงโดยไม่ต้องเปิดเซสชันอุปกรณ์อื่น บัญชีแบบอ่านอย่างเดียวจะได้รับ forbidden

DeviceInfo

ข้อมูลอุปกรณ์แบบคงที่ รายงานหนึ่งครั้งใน register และจัดเก็บไว้ใน devices.info ทุกอย่างยกเว้น agent_version เป็นทางเลือก: แหล่งที่มาส่วนใหญ่จะไม่พร้อมใช้งานเมื่อ agent ทำงานภายนอก OpenWrt เช่น บนเครื่องพัฒนาหรือในคอนเทนเนอร์ การลงทะเบียนควรจะสำเร็จมากกว่าการเริ่มต้นล้มเหลว ช่องที่ขาดหายไปจะถูกละเว้นระหว่างการทำให้เป็นอนุกรม

สนาม ประเภท แหล่งที่มา
hostname string? /proc/sys/kernel/hostname
model string? DISTRIB_TARGET ใน /etc/openwrt_release ถอยกลับไปเป็น model จาก ubus call system board ถ้าไม่พร้อมใช้งาน
openwrt_version string? DISTRIB_DESCRIPTION หรือ ubus release.description
kernel string? /proc/sys/kernel/osrelease
arch string? ค่าคงที่เวลาคอมไพล์ เชื่อถือได้มากกว่าการตรวจจับรันไทม์
agent_version string เวอร์ชัน agent ใช้เพื่อวินิจฉัยเวอร์ชันที่ไม่ตรงกัน

Metrics

รวบรวมอีกครั้งสำหรับแต่ละฮาร์ตบีต โดยใช้เฉพาะค่าที่มีอยู่โดยตรงจาก /proc โดยไม่ต้องฟอร์กกระบวนการย่อย ความล้มเหลวในการอ่านรายการเดียวไม่ได้ทำให้คอลเลกชันทั้งหมดล้มเหลว: การสูญเสียการวัดหนึ่งรายการจะดีกว่าความล้มเหลวของฮาร์ทบีททั้งหมด และการรายงานอุปกรณ์ออฟไลน์อย่างไม่ถูกต้อง

สนาม ประเภท คำอธิบาย
uptime_secs u64 /proc/uptime
load [f32; 3] โหลดเฉลี่ย 1/5/15 นาทีจาก /proc/loadavg
mem_total_kb u64 /proc/meminfo
mem_available_kb u64 ใช้ MemAvailable แทนที่จะเป็น MemFree: อย่างหลังไม่รวมแคชที่เรียกคืนได้และประเมินหน่วยความจำที่มีอยู่บนเราเตอร์ต่ำเกินไปอย่างมาก ทำให้เกิดการแจ้งเตือนที่ผิดพลาด
cpu_usage_pct f32? คำนวณจากความแตกต่างระหว่างสองตัวอย่าง /proc/stat ละเว้นในการเต้นของหัวใจครั้งแรกเนื่องจากไม่มีตัวอย่างอ้างอิง
interfaces InterfaceStats[] /proc/net/dev. รายงาน cumulative รับ/ส่งไบต์ แทนที่จะเป็นอัตรา ปล่อยให้เซิร์ฟเวอร์มีอิสระในการคำนวณอัตราในช่วงเวลาใดก็ได้ ละเว้นเมื่อว่างเปล่า

InterfaceStats: { name: string, rx_bytes: u64, tx_bytes: u64 }


เฟรมข้อมูลไบนารี

 0        1                    5                    9
 +--------+--------------------+--------------------+
 |  kind  |     session_id     |    payload_len     |  payload (payload_len 字节)
 |   u8   |    u32 (大端)      |    u32 (大端)      |
 +--------+--------------------+--------------------+
 └──────────── 9 字节定长头 ────────────┘
ออฟเซ็ต ความยาว สนาม คำอธิบาย
0 1 kind ประเภทสตรีม; ดูด้านล่าง
1 4 session_id บิ๊กเอนด์ u32. 0 ถูกสงวนไว้เป็น Sentinel ซึ่งหมายความว่าไม่มีเซสชัน
5 4 payload_len Big-endian u32 จำนวนไบต์ของเพย์โหลด สูงสุด 1 MiB
9 payload_len payload ไบต์ดิบโดยไม่มีการเข้ารหัสหรือการหลบหนี

ประเภทสตรีม

ความคุ้มค่า ชื่อ ทิศทาง คำอธิบาย
1 ExecStdout อุปกรณ์ → ตัวดำเนินการ เอาต์พุตมาตรฐานคำสั่ง Remote
2 ExecStderr อุปกรณ์ → ตัวดำเนินการ ข้อผิดพลาดมาตรฐาน ส่งแยกจาก stdout เพื่อให้ CLI สามารถแสดงผลแตกต่างออกไป เช่น เป็นสีแดง
3 PtyData สองทิศทาง สตรีมไบต์ PTY ต้นน้ำนำอินพุตคีย์บอร์ด ปลายน้ำนำเอาต์พุตหน้าจอ ประเภทเดียวกันให้บริการทั้งสองทิศทาง

หาก agent ได้รับ ExecStdout / ExecStderr ดาวน์สตรีม ระบบจะบันทึกคำเตือนและละทิ้งเฟรม ซึ่งบ่งชี้ถึงข้อผิดพลาดในการใช้งานที่เพียร์

หมายเหตุการออกแบบ

**payload_len นั้นซ้ำซ้อนกับฟิลด์ความยาว WebSocket ** มันยังคงอยู่ด้วยเหตุผลสองประการ: การถอดรหัสสามารถตรวจจับการตัดทอน (การประกาศ 11 ไบต์โดยมีเพียง 5 ที่ให้มาจะต้องถูกปฏิเสธ แทนที่จะยอมรับโดยไม่แจ้งให้ทราบว่าเป็นแพ็กเก็ตที่สั้นกว่า) และรูปแบบเฟรมจะยังคงไม่เปลี่ยนแปลงหากจำเป็นต้องใช้ TCP แบบดิบในภายหลัง

ขีดจำกัดเพย์โหลด 1 MiB ไม่ได้มีไว้สำหรับการรับส่งข้อมูลปกติ: PTY และเอาต์พุตคำสั่งเป็นสตรีมส่วนเพิ่มที่เล็กกว่านั้นมาก ป้องกันไม่ให้เพียร์ใช้หน่วยความจำเซิร์ฟเวอร์จนหมดด้วยส่วนหัวที่อ้างว่ามีเพย์โหลด 2 GiB

เพย์โหลด Empty ถูกต้อง: การอ่านแบบศูนย์ไบต์บางครั้งเกิดขึ้นในเซสชัน PTY เฟรมดังกล่าวประกอบด้วยส่วนหัวขนาด 9 ไบต์เพียงอย่างเดียว

เพย์โหลดมีไบต์ที่กำหนดเอง รวมถึงลำดับที่ไม่ใช่ UTF-8 และต้องได้รับการเก็บรักษาไว้อย่างถูกต้อง

ข้อผิดพลาดในการถอดรหัส

เกิดข้อผิดพลาด ทริกเกอร์
FrameTooShort { len } ความยาวรวมต่ำกว่า 9 ไบต์
UnknownStreamKind(v) ไบต์แรกไม่ใช่ 1/2/3
PayloadTooLarge { len } ประกาศ payload_len เกิน 1 MiB
LengthMismatch { declared, actual } ความยาวที่ประกาศแตกต่างจากจำนวนไบต์ที่เหลืออยู่จริง

ความล้มเหลวในการถอดรหัส data-frame ส่งผลต่อเฟรมนั้นเท่านั้น: exchange บันทึกคำเตือนและละทิ้งโดยไม่ยกเลิกการเชื่อมต่อ


การแปลรหัสเซสชันระหว่างสองเนมสเปซ

นี่คือส่วนหนึ่งของโปรโตคอลที่เข้าใจผิดได้ง่ายที่สุด

ในประโยคเดียว

initiator จัดสรร session_id ซึ่งเป็น unique ภายในการเชื่อมต่อของตัวเองเท่านั้น การเชื่อมต่อระหว่างโอเปอเรเตอร์และอุปกรณ์จะรักษาเนมสเปซ ID อิสระ และ exchange จะแปลระหว่างทั้งสอง เซสชันโลจิคัลหนึ่งเซสชันมี ID ที่แตกต่างกันสองรหัส

  remote-cli                    exchange                      remote-agent
                                                        (设备 lab-router)

  ┌────────────┐        ┌────────────────────┐            ┌────────────┐
  │ 运维编号   │        │      路由表        │            │ 设备编号   │
  │  空间      │        │                    │            │  空间      │
  │            │        │  lab-router:       │            │            │
  │  session 7 │───────▶│    31 → (op-A, 7)  │───────────▶│ session 31 │
  │            │◀───────│                    │◀───────────│            │
  └────────────┘        │  op-A:             │            └────────────┘
                        │    7 → (lab-router,│
   由 CLI 自己的        │         31)        │             由 exchange
   SessionIdGenerator   └────────────────────┘             的 DeviceHandle
   分配(1,2,3…)                                          分配(1,2,3…)

ทำไมต้องออกแบบนี้

**Aหลีกเลี่ยงการเดินทางไปกลับ ** หากเซิร์ฟเวอร์จัดสรร ID ทั้งหมด CLI จะต้องรอการตอบรับโดยแจ้งว่า ID เซสชันคือ 31 ก่อนที่จะส่งอินพุตคีย์บอร์ดหลังจากเปิด PTY การจัดสรร CLI อนุญาตเฟรมข้อมูลทันทีหลังจาก open_pty

**Aหลีกเลี่ยงการชนกันของ ID ระหว่างตัวดำเนินการ ** ตัวดำเนินการสองคนอาจใช้ ID ท้องถิ่น 1 บนอุปกรณ์เดียวกันพร้อมกัน อุปกรณ์จะต้องแยกแยะความแตกต่างเหล่านี้เพื่อหลีกเลี่ยงการส่งเอาต์พุตของบุคคลหนึ่งไปยังอีกบุคคลหนึ่ง exchange จัดสรร ID ฝั่งอุปกรณ์แยกกันสำหรับการเชื่อมต่ออุปกรณ์แต่ละรายการ โดยจับคู่อินสแตนซ์ session 1 สองตัวกับ 31 ฝั่งอุปกรณ์และ 32

**ป้องกันพฤติกรรมของผู้ปฏิบัติงานไม่ให้ส่งผลต่อ ID ฝั่งอุปกรณ์ ** exchange ควบคุม ID ฝั่งอุปกรณ์ทั้งหมด ไม่มีผู้ปฏิบัติงานสามารถระบุหรือคาดเดาได้

สองโต๊ะ

exchange รักษาหนึ่งตารางต่อการเชื่อมต่อ โดยทั้งสองตารางจะอัปเดตร่วมกันเสมอ:

ที่ตั้ง ตาราง คีย์ → ค่า
DeviceHandle.sessions โต๊ะข้างหน้าฝั่งอุปกรณ์ ID เซสชั่นอุปกรณ์ → (ตัวดำเนินการส่ง, ID เซสชั่นของโอเปอเรเตอร์, ID บัญชีของโอเปอเรเตอร์, ประเภทเซสชั่น, ID แถวการตรวจสอบ)
OperatorHandle.sessions ดัชนีย้อนกลับด้านตัวดำเนินการ รหัสเซสชันของผู้ปฏิบัติงาน → (รหัสอุปกรณ์ รหัสเซสชันอุปกรณ์)

ดัชนีย้อนกลับช่วยให้สามารถล้างข้อมูลได้: เมื่อผู้ปฏิบัติงานตัดการเชื่อมต่อ เซิร์ฟเวอร์จะต้องค้นหาและยุติเซสชันบนอุปกรณ์ e ทุกเครื่อง ไม่เช่นนั้น กระบวนการและ PTY ที่ไม่มีผู้รับเอาต์พุตจะยังคงอยู่

การแปลเกิดขึ้นที่ไหน

ทิศทาง ข้อความ การแปล
ผู้ดำเนินการ → อุปกรณ์ (open_*) ข้อความควบคุม Allocate ID ใหม่ในเนมสเปซอุปกรณ์ ลงทะเบียนในทั้งสองตาราง และใช้ ID ใหม่ในข้อความที่ส่งไปยังอุปกรณ์
ผู้ดำเนินการ → อุปกรณ์ (pty_resize / close_session) ข้อความควบคุม ค้นหารหัสเซสชันของอุปกรณ์ผ่านดัชนีย้อนกลับ แทนที่และส่งต่อ
ผู้ประกอบการ → อุปกรณ์ กรอบข้อมูล ค้นหารหัสเซสชันของอุปกรณ์ผ่านดัชนีแบบย้อนกลับและเขียนทับไบต์ของส่วนหัวทั้ง 4 ไบต์ โดยไม่เปลี่ยนแปลงเพย์โหลด
อุปกรณ์ → ตัวดำเนินการ ข้อความควบคุม ค้นหารหัสเซสชันของผู้ปฏิบัติงานและการเชื่อมต่อเป้าหมายในตารางส่งต่อ แทนที่รหัส และส่งมอบ
อุปกรณ์ → ตัวดำเนินการ กรอบข้อมูล ค้นหารหัสเซสชันของโอเปอเรเตอร์ในตารางไปข้างหน้าและเขียนทับส่วนหัวแทน โดยไม่เปลี่ยนแปลงเพย์โหลด

การแปลเฟรมข้อมูลคือการเขียนใหม่ only: exchange จะไม่แยกวิเคราะห์ บัฟเฟอร์ หรือเปลี่ยนเพย์โหลดไบต์ใดๆ DataFrame::with_session_id แทนที่ ID ที่มีอยู่โดยไม่ต้องจัดสรรเพย์โหลดใหม่

กฎการจัดสรร

ตัวจัดสรรทั้งสองเพิ่มขึ้นจาก 1 และ skip 0 ซึ่งเป็น no-session sentinel ที่ใช้ใน ตัวอย่างเช่น ในข้อความแสดงข้อผิดพลาดระดับการเชื่อมต่อ

  • CLI (SessionIdGenerator): wraps บนโอเวอร์โฟลว์และข้าม 0 การใช้สี่พันล้านเซสชันในการเชื่อมต่อเดียวนั้นไม่สมจริง แต่การ wraps เหมาะกับกระบวนการที่ใช้เวลานานมากกว่าการตื่นตระหนก
  • อุปกรณ์ (DeviceHandle::open_session): หลังจากสรุปแล้ว หาก ID อยู่ในเซสชันสด ให้ดำเนินการต่อไปยังช่องว่างถัดไป

exchange ปฏิเสธ ID ตัวดำเนินการ 0 และการนำ ID เซสชันที่ใช้งานอยู่กลับมาใช้ใหม่ ความถูกต้องของการเชื่อมต่ออุปกรณ์ ขีดจำกัดการทำงานพร้อมกัน และการลงทะเบียนในดัชนีทั้งสองจะได้รับการตรวจสอบภายในส่วนสำคัญเดียวกัน ป้องกันไม่ให้เซสชันที่เปิดพร้อมกันจากผู้ให้บริการหลายรายเกินขีดจำกัด

เมื่อไม่มีเส้นทาง

** ทิ้งอย่างเงียบๆ โดยไม่มีข้อผิดพลาด ** เพียร์ที่ยังคงส่งข้อมูลต่อท้ายหลังจากปิดเซสชันถือเป็นการแข่งขันปกติ สิ่งนี้ใช้ได้กับทั้งสองทิศทาง:

  • อุปกรณ์ตอบกลับสำหรับเซสชันที่ไม่มีอยู่อีกต่อไป → บันทึกการแก้ไขข้อบกพร่อง ผู้ดำเนินการอาจตัดการเชื่อมต่อไปแล้ว
  • ผู้ดำเนินการส่งข้อมูลสำหรับเซสชันที่ไม่ได้รับอนุญาต → ติดตามบันทึกและละทิ้ง

เส้นทางการล้างข้อมูลเซสชัน

ทริกเกอร์ การล้างข้อมูล
อุปกรณ์สิ้นสุดเซสชันตามปกติ (exec_exit / ubus_resp / android_resp / pty_closed / session_error) ลบเส้นทางไปข้างหน้าและดัชนีย้อนกลับของการเชื่อมต่อที่เป็นเจ้าของพร้อมกัน อัปเดตบันทึกการตรวจสอบ และส่งข้อความที่แปลแล้วไปยังผู้ปฏิบัติงาน
โอเปอเรเตอร์ close_session ลบดัชนีทั้งสองออก แจ้งให้การเชื่อมต่ออุปกรณ์เดิมยุติ รับทราบการปิดตัวต่อผู้ปฏิบัติงานทันที และอัปเดตบันทึกการตรวจสอบการยกเลิก การปิดซ้ำไม่ทำอะไรเลย
อุปกรณ์ตัดการเชื่อมต่อ ใช้เซสชันทั้งหมดสำหรับการเชื่อมต่อนั้น ส่ง session_failed / device_disconnected ไปยังผู้ให้บริการแต่ละราย และอัปเดตบันทึกการตรวจสอบด้วย {"outcome":"device_disconnected"}
ตัวดำเนินการตัดการเชื่อมต่อ สำรวจดัชนีย้อนกลับเพื่อค้นหาเซสชันบนอุปกรณ์แต่ละเครื่อง ส่งคำแนะนำในการยุติสำหรับเซสชันที่ไม่ระงับ สำหรับ Hold exec ส่งเฉพาะ exec_detach เพื่อให้กระบวนการทำงานต่อไป ล้างเฉพาะเซสชันของตัวเอง; ผู้ให้บริการรายอื่นบนอุปกรณ์เดียวกันจะไม่ได้รับผลกระทบ
อุปกรณ์เชื่อมต่อใหม่และแทนที่การเชื่อมต่อเก่า ทำให้เซสชันทั้งหมดในการเชื่อมต่อเก่าใช้ไม่ได้ และแจ้งให้ผู้ให้บริการแต่ละรายทราบด้วย device_disconnected

ดัชนีย้อนกลับเชื่อมโยงกับการเชื่อมต่ออุปกรณ์เฉพาะ จะไม่เปลี่ยนเส้นทางตามรหัสอุปกรณ์ไปยังการเชื่อมต่อใหม่หลังจากการเชื่อมต่อใหม่ การเชื่อมต่อของผู้ให้บริการหลายรายสำหรับบัญชีหนึ่งแต่ละบัญชีเป็นเจ้าของดัชนี ดังนั้นการยกเลิกการเชื่อมต่อจะไม่ยกเลิกเซสชันของการเชื่อมต่ออื่น

การรับทราบสำหรับการยกเลิกอย่างชัดเจน: exec ส่งคืน exec_exit (code: null, timed_out: false), PTY ส่งคืน pty_closed (code: null) และ ubus ส่งคืน session_failed (reason: device_error) นี่เป็นการยืนยันว่า exchange ได้ปิดเซสชันแล้ว ไม่ใช่ว่ากระบวนการระยะไกลได้ออกจากไปแล้ว ข้อมูลต่อท้ายและข้อความออกที่ตามมาจากอุปกรณ์จะถูกละทิ้ง

ที่ขีดจำกัดการทำงานพร้อมกัน Exchange จะเรียกคืนเส้นทางที่ตัวดำเนินการหายไปหรือเกินกำหนดเวลาภายใต้การล็อกเซสชันเดียวกันก่อน จากนั้นจึงตรวจสอบขีดจำกัดอีกครั้ง กำหนดเวลาของผู้บริหารคือขีดจำกัดเวลาจัดส่งจริงบวกกับระยะเวลาผ่อนผัน 15 วินาที ไฟล์ใช้เวลา 3600 วินาทีบวก 15 วินาที ประเภทอื่นใช้งานได้ 24 ชม. Hold exec ภายในกำหนดเวลาจะไม่ถูกฆ่าเมื่อผู้ปฏิบัติงานออกไป การเรียกคืน/อย่างชัดเจน kill_device_session จะส่งการยกเลิกที่เกี่ยวข้องไปยังอุปกรณ์และลบดัชนีทั้งสองออกทันทีโดยไม่ต้องรอการตอบสนองของอุปกรณ์ ตัวดำเนินการที่พร้อมใช้งานจะได้รับ exec_exit (timed_out:true) สำหรับ exec หรือ session_failed (device_error, session reclaimed) สำหรับประเภทอื่น การอัปเดตการตรวจสอบเป็นแบบอะซิงโครนัสและพยายามอย่างเต็มที่

การเชื่อมต่ออุปกรณ์ใหม่และยุค

การเชื่อมต่ออุปกรณ์แต่ละรายการจะได้รับ epoch (UUID) แบบสุ่มระหว่างการลงทะเบียน เมื่ออุปกรณ์เดียวกันเชื่อมต่อใหม่ ** การเชื่อมต่อใหม่จะแทนที่อันเก่า** แทนที่จะถูกปฏิเสธ เป็นไปไม่ได้ที่จะแยกแยะการเชื่อมต่อที่ซ้ำกันจากการเชื่อมต่อที่ไม่ทำงานซึ่งเซิร์ฟเวอร์ตรวจไม่พบ (การเชื่อมต่อแบบเปิดครึ่งหนึ่ง) ได้อย่างน่าเชื่อถือ การปฏิเสธอันใหม่จะป้องกันการเชื่อมต่อใหม่จนกว่าอันเก่าจะหมดเวลา

การล้างข้อมูลสำหรับการเชื่อมต่อเก่าอาจดำเนินการลงทะเบียน after ของการเชื่อมต่อใหม่ การยกเลิกการลงทะเบียนจึงต้องตรวจสอบยุค: ลบรายการตารางการเชื่อมต่อเฉพาะเมื่อยุคปัจจุบันตรงกับการเชื่อมต่อที่กำลังล้างข้อมูล หากไม่มีการตรวจสอบนี้ การล้างข้อมูลการเชื่อมต่อเก่าจะทำเครื่องหมายอุปกรณ์ที่เชื่อมต่อใหม่เป็นออฟไลน์ทันที


ลำดับการจับมือ

ด้านอุปกรณ์

  remote-agent                                        exchange
      │                                                 │
      │  ①  WebSocket 握手(wss://host/ws/device)      │
      │────────────────────────────────────────────────▶│
      │◀────────────────────────────────────────────────│
      │            101 Switching Protocols              │
      │                                                 │
      │                                          ┌──────┴──────┐
      │                                          │ 启动 15 秒  │
      │                                          │ 注册计时器  │
      │                                          └──────┬──────┘
      │  ②  {"type":"register","protocol_version":1,    │
      │      "device_id":"…","token":"…","info":{…}}    │
      │────────────────────────────────────────────────▶│
      │                                                 │
      │                              ┌──────────────────┴──────────────────┐
      │                              │ a. 比对 protocol_version            │
      │                              │ b. split_token → 前缀查 devices 表  │
      │                              │ c. 校验 enabled                     │
      │                              │ d. argon2 verify(前缀不符也照做,  │
      │                              │    避免用耗时区分前缀是否存在)     │
      │                              └──────────────────┬──────────────────┘
      │                                                 │
      │  ③  {"type":"register_ack","ok":true,           │
      │      "error":null,"heartbeat_interval_secs":30} │
      │◀────────────────────────────────────────────────│
      │                                                 │
      │                                          ┌──────┴──────┐
      │                                          │ 登记连接表  │
      │                                          │ 顶替旧连接  │
      │                                          │ 回写 info 与│
      │                                          │ last_seen_at│
      │                                          └──────┬──────┘
      │                                                 │
      │  ④  退避曲线复位,写状态文件 state=connected     │
      │                                                 │
      │══════════════ 转发循环 ═══════════════════════════│
      │                                                 │
      │  heartbeat(每 30 秒,携带 Metrics)             │
      │────────────────────────────────────────────────▶│
      │  WebSocket Ping(随每次 heartbeat 发出)         │
      │────────────────────────────────────────────────▶│
      │                                     WebSocket Pong
      │◀────────────────────────────────────────────────│
      │                                     WebSocket Ping(每 30 秒)
      │◀────────────────────────────────────────────────│
      │  Pong(tungstenite 排队,agent 显式 flush)       │
      │────────────────────────────────────────────────▶│

เส้นทางความล้มเหลว:

  • เฟรมแรกไม่ใช่ข้อความ เป็น JSON ไม่ถูกต้อง หรือไม่ใช่ register → บันทึกคำเตือนและปิดทันทีโดยไม่มีแอก
  • ไม่มี register ภายใน 15 วินาที → ปิด
  • เวอร์ชันไม่ตรงกัน → ส่งคืน register_ack{ok:false, error:"协议版本不匹配:设备为 X,服务端为 Y"} และปิด
  • โทเค็นไม่ถูกต้อง / อุปกรณ์ไม่มีอยู่ / อุปกรณ์ที่ปิดใช้งาน → ส่งคืน "设备 ID 或令牌无效" เดิมเสมอและปิด อย่าแยกแยะสาเหตุ: การทำเช่นนั้นจะทำให้สามารถนับรายการอุปกรณ์ได้

agent จัดการหมวดหมู่ความล้มเหลวทั้งสองนี้แยกกัน:

หมวด agent ทริกเกอร์ กลยุทธ์การเชื่อมต่อใหม่
Rejected รับ ack ด้วย ok: false แก้ไขการรอ 300 วินาที การลองอีกครั้งไม่สามารถแก้ไขโทเค็นที่ไม่ถูกต้องได้ ลองใหม่บ่อยครั้งโดยสิ้นเปลืองทรัพยากรทั้งสองด้านและบันทึกเซิร์ฟเวอร์ท่วม การรอที่นานกว่าจะทำให้ผู้อื่นมีเวลาอัปเดตการกำหนดค่า
Transport การเชื่อมต่อล้มเหลว หมดเวลาการจับมือ (45 วินาที) การตอบสนองมีรูปแบบไม่ถูกต้อง การเชื่อมต่อถูกขัดจังหวะ Exponential Backoff: เริ่มต้นที่ 1 วินาที เพิ่มเป็นสองเท่าในแต่ละครั้งสูงสุด 300 วินาที คูณด้วยความกระวนกระวายใจแบบสุ่มใน [0.5, 1.5)

**Backoff จะรีเซ็ตหลังจากการลงทะเบียนสำเร็จแล้วเท่านั้น ** ไม่ใช่เฉพาะการสร้าง TCP เท่านั้น มิฉะนั้น การปฏิเสธทันทีหลังจากการเชื่อมต่อจะลดลงเป็นการลองใหม่อย่างหนาแน่นในช่วงเวลามิลลิวินาที

**Jitter เป็นสิ่งจำเป็น ** เมื่อเซิร์ฟเวอร์รีสตาร์ท อุปกรณ์หลายร้อยหรือหลายพันเครื่องจะตรวจพบการตัดการเชื่อมต่อพร้อมกัน ด้วยกราฟการลองซ้ำตามที่กำหนดที่เหมือนกัน พวกมันยังคงซิงโครไนซ์และโจมตีเซิร์ฟเวอร์ที่เพิ่งเริ่มต้นใหม่เป็นระลอก เปลี่ยนการรีสตาร์ทเป็นความล้มเหลวแบบเรียงซ้อนอย่างต่อเนื่อง

ฝั่งโอเปอเรเตอร์

  remote-cli                                          exchange
      │                                                 │
      │  ①  WebSocket 握手(wss://host/ws/operator)    │
      │      Authorization: Bearer <prefix>.<secret>    │
      │────────────────────────────────────────────────▶│
      │                                                 │
      │                              ┌──────────────────┴──────────────────┐
      │                              │ 在 upgrade 之前完成鉴权:           │
      │                              │ split_token → 前缀查 operators 表   │
      │                              │ → argon2 verify → 校验 enabled      │
      │                              └──────────────────┬──────────────────┘
      │                                                 │
      │      鉴权失败 → HTTP 401「凭证无效」(不升级)   │
      │◀────────────────────────────────────────────────│
      │                                                 │
      │◀────────────────────────────────────────────────│
      │            101 Switching Protocols              │
      │                                                 │
      │  ②  {"type":"welcome","protocol_version":1,     │
      │      "server_version":"0.1.0"}                  │
      │◀────────────────────────────────────────────────│
      │                                                 │
      │  ③  比对 protocol_version(15 秒超时)          │
      │      不一致 → 报错退出,提示升级 CLI            │
      │                                                 │
      │══════════════ 可以发起会话 ═══════════════════════│
      │                                                 │
      │  {"type":"open_pty","session_id":1,             │
      │   "device_id":"lab-router","rows":40,"cols":120}│
      │────────────────────────────────────────────────▶│
      │                                                 │──▶ 查设备在线
      │                                                 │──▶ 查并发上限
      │                                                 │──▶ 查 readonly 角色
      │                                                 │──▶ 写审计(先于下发)
      │                                                 │──▶ 分配设备侧编号
      │  二进制帧(PtyData, session 1, 键盘输入)        │
      │────────────────────────────────────────────────▶│──▶ 改写为设备侧编号
      │                                                 │
      │  二进制帧(PtyData, session 1, 屏幕输出)        │
      │◀────────────────────────────────────────────────│◀── 改写为运维侧编号

CLI สามารถส่งเฟรมข้อมูลถัดไป ** โดยไม่ต้องรอการตอบรับใดๆ** นี่เป็นสาเหตุที่แน่ชัดว่าเหตุใดผู้ริเริ่มจึงจัดสรร ID เซสชัน


หมวดหมู่การจัดการข้อผิดพลาดและความล้มเหลว

SessionFailure

ใช้ในฟิลด์ reason ของ session_failed; ค่า JSON ใช้ Snake_case

ความคุ้มค่า ความหมาย ผู้ผลิต ลองใหม่ได้
device_offline ไม่มีอุปกรณ์อยู่หรือออฟไลน์อยู่ในขณะนี้ exchange ไม่พบอุปกรณ์ในตารางการเชื่อมต่อระหว่าง open_* ✅
device_disconnected อุปกรณ์ถูกตัดการเชื่อมต่อระหว่างเซสชัน อุปกรณ์ถูกตัดการเชื่อมต่อ / แทนที่ด้วยการเชื่อมต่อใหม่ / คิวการส่งปิดไปแล้วระหว่างการจัดส่ง ✅
device_error อุปกรณ์ปฏิเสธคำขอหรือการดำเนินการล้มเหลว agent ส่งคืน session_error (การสร้าง PTY การเริ่มต้นคำสั่ง หรือการเรียก ubus ล้มเหลว) หรือเพียร์กินช้าเกินไปในระหว่างการส่งต่อ ❌
rate_limited เกินขีดจำกัดเซสชันที่เกิดขึ้นพร้อมกัน exchange ค้นหาเซสชันที่ใช้งานของอุปกรณ์ถึง --max-sessions-per-device (ค่าเริ่มต้น 32) ระหว่าง open_* ✅
forbidden ข้อมูลประจำตัวของผู้ให้บริการไม่สามารถเข้าถึงอุปกรณ์ได้ บัญชีแบบอ่านอย่างเดียว (role = readonly) พยายามเริ่มต้นเซสชันใดๆ ❌

is_retryable() จงใจยกเว้น forbidden และ device_error: การลองปัญหาการอนุญาตอีกครั้งหรือข้อผิดพลาดของอุปกรณ์ภายในไม่มีจุดหมายและทำให้เกิดกิจกรรม CLI ที่ไม่จำเป็นเท่านั้น

CLI ผนวกคำใบ้ที่สามารถดำเนินการได้ต่อท้าย message ดั้งเดิม ตัวอย่างเช่น device_offline แนะนำให้ตรวจสอบสถานะอุปกรณ์ด้วย remote-cli device list device_error เป็นข้อยกเว้น: ข้อผิดพลาดของอุปกรณ์เองเป็นคำอธิบายที่ถูกต้องที่สุด และไม่ควรเจือจางด้วยแผ่นสำเร็จรูป

ความหมายความล้มเหลวตามเลเยอร์

เลเยอร์ พฤติกรรม ขอบเขต
การถอดรหัสเฟรมล้มเหลว เข้าสู่ระบบและทิ้งเฟรม หนึ่งเฟรม
ควบคุมการแยกวิเคราะห์ข้อความล้มเหลว เตือนและเพิกเฉยต่อข้อความ หนึ่งข้อความ; การเชื่อมต่อดำเนินต่อไป
ความล้มเหลวระดับเซสชัน session_failed / session_error หนึ่งเซสชั่น; การเชื่อมต่อดำเนินต่อไป
การลงทะเบียนล้มเหลว ปิดหลัง register_ack{ok:false} การเชื่อมต่อทั้งหมด
เวอร์ชันไม่ตรงกัน ปิดหลังจากข้อความแสดงข้อผิดพลาดที่ชัดเจน การเชื่อมต่อทั้งหมด
คิวล่าช้า ปิดการเชื่อมต่อเชิงรุก การเชื่อมต่อทั้งหมด

หลักการ: รักษาความล้มเหลวในเครื่องทุกครั้งที่เป็นไปได้ เนื่องจากการเชื่อมต่ออุปกรณ์อาจมีเซสชันพร้อมกันสำหรับผู้ให้บริการหลายราย


Keepalive และหมดเวลา

กลไก ความคุ้มค่า คำอธิบาย
exchange WebSocket ปิง ทุก ๆ --heartbeat-secs (ค่าเริ่มต้น 30 วินาที) exchange ส่งไปยังทั้งอุปกรณ์และผู้ให้บริการ ไลบรารีไคลเอนต์จัดคิว Pong โดยอัตโนมัติ agent ต้องล้าง sink อย่างชัดเจนหลังจากได้รับ Ping เพื่อส่ง Pong ทันที
agent WebSocket ปิง ทุก heartbeat_interval_secs (มีให้ในเซิร์ฟเวอร์ ack) agent ส่ง Ping ดั้งเดิมเพิ่มเติมจาก JSON heartbeat แต่ละตัว หลีกเลี่ยงขอบเขตการยกเลิกการเชื่อมต่อที่ไม่ได้ใช้งานประมาณ 100 วินาทีของ Cloudflare หาก Ping ของ exchange ล่าช้า
หมดเวลาความเงียบของการเชื่อมต่อ heartbeat × --missed-pings (ค่าเริ่มต้น 30 × 3 = 90 วินาที) การเชื่อมต่ออุปกรณ์หรือผู้ให้บริการที่ไม่มีข้อความขาเข้าในช่วงเวลานี้จะถือว่าสูญหายและถูกตัดการเชื่อมต่อ โป่งถือเป็นกิจกรรม --missed-pings ตั้งค่าเป็น 0 ย้อนกลับไปที่ช่วงการเต้นของหัวใจอย่างน้อยหนึ่งช่วง
หมดเวลาการเขียน exchange 10 วินาที ข้อมูล WebSocket ใด ๆ หรือการหมดเวลาการเขียน Ping จะสิ้นสุดการเชื่อมต่อ ป้องกันไม่ให้การเชื่อมต่อที่ช้าบล็อกการล้างข้อมูล
การเต้นของหัวใจธุรกิจ ทุก heartbeat_interval_secs (มีให้ในเซิร์ฟเวอร์ ack) agent ส่ง JSON heartbeat พร้อมด้วย Metrics และ WebSocket Ping แยกต่างหาก การรายงานตัวชี้วัดและปิงปอง/ปิงปองดั้งเดิมมีจุดประสงค์ที่แยกจากกัน จังหวะทางธุรกิจเพียงอย่างเดียวไม่สามารถระบุสถานะการเชื่อมต่อได้
กิจกรรมออนไลน์/ออฟไลน์ tick_count สำหรับทริกเกอร์การแจ้งเตือน online / offline (ค่าเริ่มต้น 3, ช่วง 1–100); การออกอากาศ/การตรวจสอบทางเลือก REMOTE_MISSED_THRESHOLD / REMOTE_ONLINE_THRESHOLD ผู้ปฏิบัติงานกำหนดค่าทริกเกอร์สองตัว แทนที่จะนับสองครั้งในทริกเกอร์ตัวเดียว offline แจ้งเตือนหลังจาก tick_count พลาดติดต่อกัน online แจ้งเตือนหลังจาก tick_count สำเร็จติดต่อกัน device_ids ของทริกเกอร์แต่ละตัวสามารถเป็น ID อุปกรณ์ (สตริง), ชุด ID (อาร์เรย์) หรือละไว้ / null สำหรับอุปกรณ์พื้นที่ทำงานทั้งหมด ทริกเกอร์ไม่แจ้งเตือนซ้ำในขณะที่อยู่ในสถานะรายงานแล้ว ก่อนอื่นจะต้องทำการจัดเรียงใหม่ผ่านผลลัพธ์ตรงกันข้ามที่ติดต่อกันของ tick_count การออกอากาศสถานะและการตรวจสอบสถานะในกระบวนการไม่อ่านทริกเกอร์เหล่านี้และถอยกลับไปเป็น REMOTE_MISSED_THRESHOLD / REMOTE_ONLINE_THRESHOLD (ยังมีค่าเริ่มต้นเป็น 3) ซ็อกเก็ตจะยังคงอยู่ในตารางการเชื่อมต่อหรือไม่นั้นจะถูกกำหนดแยกต่างหากโดย --missed-pings
หมดเวลาการลงทะเบียนอุปกรณ์ 15 วินาที ต้องส่ง register ภายในช่วงเวลานี้หลังจากสร้างการเชื่อมต่อ
หมดเวลาการจับมือ agent 45 วินาทีต่อเฟส (HANDSHAKE_TIMEOUT) connect_device_ws (TCP, ตัวเลือก HTTP CONNECT, TLS, WebSocket handshake) และรอ register_ack หลังจากส่ง register แต่ละรายการจะได้รับขีดจำกัดนี้ 45 วินาทีไม่ใช่ขีดจำกัดรวม
agent เขียน / หมดเวลาล้าง Pong 30 วินาที (WRITE_TIMEOUT) การส่งแต่ละครั้ง (การลงทะเบียน, ฮาร์ทบีท JSON, Ping, เอาต์พุตเซสชัน) และ Pong flush หลังการลงทะเบียนแต่ละครั้งจะมีกำหนดเวลาของตัวเอง การเขียนล้มเหลวหรือการหมดเวลาจะสิ้นสุดการเชื่อมต่อและทริกเกอร์การเชื่อมต่อใหม่ โป่งฟลัชขณะรอการลงทะเบียนจะถูกควบคุมโดยการรอ 45 วินาทีด้านบน
agent TCP Keepalive ตรวจสอบหลังจากไม่ได้ใช้งานเป็นเวลา 30 วินาที เปิดใช้งาน TCP_NODELAY และ TCP Keepalive หลังจากการเชื่อมต่อโดยตรงหรือ HTTP CONNECT การตั้งค่า TCP ไม่ได้แทนที่ WebSocket Ping/Pong
หมดเวลาต้อนรับ CLI 15 วินาที เวลาที่ CLI รอ welcome หลังจากการเชื่อมต่อ
หมดเวลาดำเนินการคำสั่ง ระบุโดย timeout_secs ใน open_exec agent หนีบไว้อย่างน้อย 1 วินาที เมื่อหมดเวลา SIGTERM กลุ่มกระบวนการทั้งหมด จากนั้น SIGKILL หลังจากช่วงเวลาผ่อนผัน 3 วินาที
หมดเวลาการโทรของ ubus 15 วินาที (คงที่ภายใน agent) ubus คือ RPC ท้องถิ่น โดยทั่วไปแล้วการเกินช่วงเวลานี้บ่งชี้ว่าบริการที่เกี่ยวข้องนั้นค้างอยู่

ไคลเอ็นต์เชลล์ที่ไม่ได้ใช้งานยังคงตอบกลับด้วย Pong agent ล้างการตอบสนองโดยอัตโนมัติซึ่งจัดคิวโดยไลบรารีอย่างชัดเจน โดยไม่ต้องรอจังหวะการเต้นของหัวใจทางธุรกิจครั้งถัดไป ดังนั้น การไม่มีอินพุตคีย์บอร์ดจึงไม่ทำให้เกิดการตัดการเชื่อมต่อที่ผิดพลาด (#42) --heartbeat-secs ต้องมากกว่า 0; ค่าที่ไม่ถูกต้องจะถูกปฏิเสธในระหว่างการแยกวิเคราะห์อาร์กิวเมนต์เริ่มต้น


แรงดันย้อนกลับ

exchange รักษาคิวการส่งแบบมีขอบเขตของข้อความ 512 สำหรับการเชื่อมต่อแต่ละรายการ

คิวเต็มหมายความว่าเพียร์ไม่สามารถตามได้ โดยปกติแล้วเนื่องจากการเชื่อมต่อ PTY หยุดทำงานหรือไคลเอนต์ถูกระงับ การตอบสนองคือ ** ยกเลิกการเชื่อมต่อนั้นในเชิงรุก** แทนที่จะเก็บบัฟเฟอร์ไว้: การบัฟเฟอร์ที่ไม่จำกัดจะทำให้ไคลเอ็นต์ที่ค้างอยู่หนึ่งเครื่องกลายเป็นหน่วยความจำรั่วซึ่งส่งผลต่อบริการทั้งหมด ข้อความ 512 มีพื้นที่ว่างเพียงพอสำหรับการรับส่งข้อมูล PTY แบบโต้ตอบ (โดยปกติความยาวของคิวจะเป็นตัวเลขหลักเดียว) โดยทั่วไปการถึงขีดจำกัดหมายความว่าเพื่อนหยุดบริโภคแล้ว

การนำส่งใช้ try_send ที่ไม่ปิดกั้นเสมอ เส้นทางการเราต์มีการล็อคตารางการเชื่อมต่อ await ใดๆ จะผูกระยะเวลาการล็อกกับความเร็วในการใช้เพียร์ ซึ่งช่วยให้ไคลเอ็นต์ที่ช้าสามารถหยุดทั้งตารางได้

ความล้มเหลวในการจัดส่งทั้งสองรายการได้รับการจัดการที่แตกต่างกัน:

ผลลัพธ์ ความหมาย การจัดการ
Lagging คิวเต็ม. ขัดจังหวะการเชื่อมต่อโดยใช้สัญญาณปิดที่แยกจากกัน ทำความสะอาดเซสชัน และใช้ความพยายามอย่างดีที่สุดเพื่อแจ้งจุดสิ้นสุดอื่น ๆ ของความล้มเหลว
Closed รีซีฟเวอร์ออกแล้ว การยกเลิกการเชื่อมต่อตามปกติ บันทึกการแก้ไขข้อบกพร่องเท่านั้น

สัญญาณการปิดการเชื่อมต่อจะข้ามคิวการส่ง ช่วยให้สามารถล้างข้อมูลได้แม้ว่าคิวจะเต็มหรือการเขียนถูกบล็อกก็ตาม การเปลี่ยนอุปกรณ์หลังจากเชื่อมต่อใหม่และการหมุนโทเค็นจะใช้กลไกเดียวกัน

agent ยังมีคิวขาออกที่มีความจุ 256 คิวเต็มหมายความว่าเครือข่ายไม่สามารถส่งข้อมูลได้เลย ดังนั้นการสะสมเพิ่มเติมจึงไม่มีประโยชน์ ตัดการเชื่อมต่อและเชื่อมต่อใหม่ เมื่อคิวอินพุต PTY (64) เต็ม การกดแป้นพิมพ์จะถูกละทิ้งไป: การสะสมอินพุตโต้ตอบขนาดเล็กนั้นไม่ได้มีวัตถุประสงค์

การควบคุมหน้าจอ Android

กำหนดใน crates/proto/src/android.rs โดยใช้การกำหนดเส้นทางเซสชันสองเนมสเปซที่มีอยู่ การรับรองความถูกต้อง ขีดจำกัดการทำงานพร้อมกัน และการตรวจสอบ request ใน open_android / android_req เป็นอ็อบเจ็กต์ JSON ที่ถูกแยกโดย action:

action สาขาอื่นๆ พฤติกรรม
status ไม่มี ค้นหาการอนุญาตท้องถิ่นและเวอร์ชัน SDK ไม่จำเป็นต้องเปิดใช้งานสวิตช์ควบคุมหน้าจอ แต่ต้องเรียกใช้บริการระยะไกล
tree ไม่มี โครงสร้างโหนดการเข้าถึงของแอปที่ใช้งานอยู่ในปัจจุบัน
screenshot ไม่มี ภาพหน้าจอ JPEG หนึ่งภาพ
tap x, y: u32 แตะพิกัดพิกเซลของหน้าจอดั้งเดิม
swipe x1, y1, x2, y2, duration_ms: u32 ปัดนิ้ว ระยะเวลา 50–5000 ms
swipe_path points: [{x, y}] (2–64 จุด), duration_ms: u32 การปัดแบบหลายส่วน: การกดเพียงครั้งเดียวจะเคลื่อนที่ผ่านทุกจุดอย่างต่อเนื่อง โดยสอดแทรกตามความยาวอย่างสม่ำเสมอ ระยะเวลา 50–5000 ms
global key: string back / home / recents / notifications / quick_settings
input_text text: string แทนที่ฟิลด์ที่แก้ไขได้ซึ่งมีขนาดสูงสุด 8192 UTF-8 ไบต์ ไม่มี NUL

AndroidResult ถูกเลือกปฏิบัติโดย kind; ต้องระบุข้อมูลทุกช่อง:

kind เขตข้อมูล
status controls_enabled, accessibility_enabled, screen_capture_enabled: bool; sdk: u32
tree nodes: AndroidNode[]; screen_width, screen_height: u32; truncated: bool
screenshot mime_type: "image/jpeg"; data_base64: string; width, height, screen_width, screen_height: u32
ok ไม่มี; บ่งบอกถึงความสมบูรณ์ของการป้อนข้อมูล ท่าทาง หรือการทำงานของระบบ

โหนดประกอบด้วยสตริง class_name, package, view_id, text, description, bounds: [left, top, right, bottom] (พิกัดพิกเซลของหน้าจอที่ลงนาม) และบูลีน clickable, editable, focused, password สูงสุด 200 โหนด โดยแต่ละสตริงจำกัดไว้ที่ 256 UTF-16 ยูนิต truncated ระบุเฉพาะขีดจำกัดของโหนด ค่ารหัสผ่านและคำอธิบายว่างเปล่า และละเว้นแผนผังย่อยการกำหนดค่าของ agent เอง

ขอบที่ยาวที่สุดของภาพหน้าจอจำกัดไว้ที่ 1600 พิกเซล width/height คือขนาดภาพ screen_width/screen_height คือขนาดก่อนการปรับขนาด พิกัดท่าทางใช้ขนาดดั้งเดิม JSON ที่ส่งคืนโดย Java Bridge ถูกจำกัดไว้ที่ 2 MiB โดยอนุญาตให้มีไบต์เพิ่มเติม 1024 ไบต์สำหรับซองจดหมาย WebSocket ไม่มีการเพิ่มประเภทเฟรมไบนารีใหม่

{"type":"open_android","session_id":1,"device_id":"phone","request":{"action":"input_text","text":"Hello\nWorld"}}
{"type":"android_req","session_id":31,"request":{"action":"input_text","text":"Hello\nWorld"}}
{"type":"android_resp","session_id":31,"result":{"kind":"ok"}}
{"type":"android_resp","session_id":1,"result":{"kind":"ok"}}

screen_capture_enabled บ่งชี้ถึงการอนุญาตภาพหน้าจอที่มีอยู่ในปัจจุบัน (MediaProjection หรือการเข้าถึง Android 11+) การใช้งานหน้าจอจริงทั้งหมดยังต้องใช้ controls_enabled สิทธิ์ไม่เพียงพอ หน้าต่างที่ไม่สามารถเข้าถึงได้ และการดำเนินการที่ล้มเหลวจะส่งคืน agent session_error ซึ่งแปลโดย Exchange เป็น session_failed เอเจนต์ที่ไม่ใช่ Android ส่งคืนข้อผิดพลาดที่ไม่สนับสนุนอย่างชัดเจน

Agent จัดการคำขอ Android ได้สูงสุด 4 รายการพร้อมกัน และเข้ารหัสภาพหน้าจอเพียงครั้งละหนึ่งรายการเท่านั้น การดำเนินการ Java รอสูงสุด 10 วินาที (สูงสุด 5 วินาทีสำหรับเฟรมหน้าจอ) การหมดเวลาคำขอ agent คือ 15 วินาที, การหมดเวลาของ CLI 20 วินาที ในการยกเลิกหรือยกเลิกการเชื่อมต่อของผู้ดำเนินการ Exchange จะส่ง android_cancel; agent ปิดซ็อกเก็ตในเครื่องและ APK จะยกเลิกงานที่อยู่ในคิวซึ่งยังไม่ได้ทำงาน ท่าทางหรือการเปลี่ยนแปลงข้อความที่เสร็จสมบูรณ์จะไม่ถูกย้อนกลับ

การดำเนินการตรวจสอบคือ android; รายละเอียดเก็บการกระทำและพิกัด input_text เก็บเฉพาะ characters (จำนวนอักขระ Unicode) และผลลัพธ์ที่สำเร็จจะบันทึกเฉพาะ outcome: "completed" เท่านั้น เนื้อหาข้อความ แผนผัง UI และรูปภาพไม่ได้ถูกเขียนลงในบันทึกการตรวจสอบ บัญชี readonly ไม่สามารถเริ่มต้นได้แม้แต่ status, tree หรือภาพหน้าจอ

ส่วนขยายเซสชันของไฟล์

เอเจนต์ที่สนับสนุนการดำเนินการกับไฟล์โฆษณาความสามารถ files Exchange แปล open_file {session_id, device_id, request} เป็น file_req {session_id, request} request.action คือ list, download หรือ upload; คำจำกัดความของฟิลด์อยู่ใน crates/proto/src/files.rs

ข้อความควบคุมแบบสองทิศทางใช้ JSON: ตัวควบคุมจะส่ง file_input {session_id, input} และอุปกรณ์ส่งคืน file_event {session_id, event} input.kind คือ chunk {offset, data}, ack {offset} หรือ finish; event.kind คือ ready {size}, chunk {offset, data}, progress {received}, complete {size} หรือ list {path, entries, truncated} เนื้อหาแบบก้อนใช้ Base64 มาตรฐาน แต่ละก้อนมีขนาดสูงสุด 64 KiB และผู้ส่งจะรอการตอบรับก่อนดำเนินการต่อ

ready เริ่มต้นของการดาวน์โหลดต้องใช้ ack {offset: 0}; ส่วนต่อมาจะรับทราบจำนวนไบต์สะสม การอัปโหลดเริ่มต้นด้วย ready และ progress แต่ละตัวจะขับเคลื่อนส่วนถัดไป หลังจากส่งทุกอย่างแล้ว finish จะคอมมิตมัน complete และ list เป็นข้อความเทอร์มินัล ข้อผิดพลาดใช้ session_error / session_failed ที่มีอยู่ close_session ส่ง file_cancel; การยกเลิกและการตัดการเชื่อมต่อทั้งลบเส้นทางเซสชัน ข้อความไฟล์จะถูกส่งต่อเฉพาะเซสชันไฟล์ โดยคงการแยกระหว่างรหัสโอเปอเรเตอร์และเซสชันอุปกรณ์

เวอร์ชันโปรโตคอลยังคงเป็น 1; ข้อความใหม่จำเป็นต้องมีการอัพเกรด Exchange และ Agent ที่ตรงกัน สำหรับขีดจำกัดการทำงานและอินเทอร์เฟซ LAN โปรดดูที่ การถ่ายโอนไฟล์และการเชื่อมต่อ LAN โดยตรง

ส่วนขยายหลายแพลตฟอร์ม

เวอร์ชันโปรโตคอลยังคงอยู่ 1. การลงทะเบียน info เพิ่มตัวเลือก platform (openwrt / linux / freebsd / netbsd / windows / android / macos) os_version และ capabilities (exec / pty / ubus / android_control / files / sensors / net) OpenWrt รายงาน openwrt_version

Heartbeat metrics.unavailable เป็นอาร์เรย์สตริงทางเลือกที่ระบุเมทริก uptime, load, memory, cpu และ interfaces ที่ไม่พร้อมใช้งานในปัจจุบัน หากไม่มี ให้แยกวิเคราะห์โดยใช้รูปแบบเก่า ค่าที่สอดคล้องกับเมตริกที่แสดงเป็นเพียงตัวยึดตำแหน่งที่เข้ากันได้เท่านั้น ลูกค้าควรแสดงว่าไม่พร้อมใช้งาน Windows ไม่ได้จัดเตรียมค่าเฉลี่ยโหลด Unix และ Android อาจจำกัดแหล่งที่มาของ /proc บางส่วนผ่านการอนุญาตของระบบ

คำสั่งถูกตีความโดยเชลล์ของอุปกรณ์เป้าหมาย: /bin/sh บน Linux/OpenWrt/macOS, PowerShell บน Windows และ /system/bin/sh บน Android รูปแบบเฟรม PTY และข้อความควบคุมเหมือนกันในทั้งสามระบบ Windows ใช้ ConPTY อุปกรณ์ที่ไม่ใช่ OpenWrt ส่งคืน session_error สำหรับคำขอ ubus

เซสชันที่ปิดผนึกตั้งแต่ต้นทางถึงปลายทาง

เวอร์ชันโปรโตคอลยังคงอยู่ 1. ข้อความใหม่: OperatorUp open_sealed {session_id, device_id, hello}, sealed_input {session_id, packet}; อุปกรณ์ลง sealed_open {session_id, hello}, sealed_input, sealed_close {session_id, detach}; อุปกรณ์ขึ้น / ตัวดำเนินการลง sealed_accept {session_id, accept}, sealed_event {session_id, packet, last} เฟรมไบนารีใหม่ประเภท 4 = Sealed (เพย์โหลด counter ‖ AEAD(inner_kind ‖ bytes)), DeviceInfo.seal_key และความสามารถ sealed

สำหรับเซสชันที่ปิดผนึก Exchange ดำเนินการเฉพาะการแปล ID การจำกัดการทำงานพร้อมกัน และการกำหนดเส้นทาง ไม่มีการแยกวิเคราะห์หรือตรวจสอบเพย์โหลด (บันทึกการตรวจสอบมีเพียงกุญแจสาธารณะ ระดับการอนุญาตและการหมดอายุ และผลลัพธ์สุดท้าย) และไม่ได้ตัดสินการอนุญาตนอกเหนือจากการตรวจสอบว่าบัญชีสามารถมองเห็นอุปกรณ์ได้หรือไม่ เส้นทางจะถูกเผยแพร่บน last:true เส้นทางที่ปิดผนึกยอมรับเฉพาะเฟรม Sealed เท่านั้น เส้นทางธรรมดาปฏิเสธเฟรม Sealed agent ละทิ้งข้อความธรรมดาใดๆ ที่ส่งไปยัง ID เซสชันที่ปิดผนึก สำหรับการอนุญาต การจับมือกัน การรับคีย์ โมเดลภัยคุกคาม โปรดดูที่ End-to-End Sealed Sessions