วิธีส่งแพ็กเก็ตเครือข่ายดิบใน Python ด้วย tun/tap

สวัสดี!

เมื่อเร็ว ๆ นี้ฉันได้ทำงานในโครงการที่ฉันใช้โปรโตคอลเครือข่ายคอมพิวเตอร์รุ่นของเล่นขนาดเล็กใน Python โดยไม่ต้องใช้ไลบรารีใด ๆ เพื่ออธิบายวิธีการทำงานของเครือข่ายคอมพิวเตอร์

ฉันยังคงเขียนโปรเจ็กต์นั้นอยู่ แต่วันนี้ฉันต้องการพูดเกี่ยวกับวิธีการทำขั้นตอนแรก: การส่งแพ็กเก็ตเครือข่ายใน Python

ในโพสต์นี้ เราจะส่งแพ็กเก็ต SYN (แพ็กเก็ตแรกในการเชื่อมต่อ TCP) จากโปรแกรม Python เล็กๆ และรับการตอบกลับจาก example.com รหัสทั้งหมดจากโพสต์นี้อยู่ใน ส่วนสำคัญ นี้

แพ็กเก็ตเครือข่ายคืออะไร

แพ็กเก็ตเครือข่ายเป็นสตริงไบต์ ตัวอย่างเช่น นี่คือแพ็กเก็ตแรกในการเชื่อมต่อ TCP:

 b'E\x00\x00,\x00\x01\x00\[email protected]\x06\x00\xc4\xc0\x00\x02\x02"\xc2\x95Cx\x0c\x00P\xf4p\x98\x8b\x00\x00\x00\x00`\x02\xff\xff\x18\xc6\x00\x00\x02\x04\x05\xb4'

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

ประเด็นคือในการส่งแพ็กเก็ตเครือข่าย เราต้องสามารถส่งและรับสตริงของไบต์ได้

ทำไมต้องปรับ / แตะ?

ปัญหาในการเขียนการใช้งาน TCP ของคุณเองบน Linux (หรือระบบปฏิบัติการใดๆ) คือ – เคอร์เนล Linux มีการใช้งาน TCP อยู่แล้ว!

ดังนั้น หากคุณส่งแพ็กเก็ต SYN บนอินเทอร์เฟซเครือข่ายปกติของคุณไปยังโฮสต์ เช่น example.com สิ่งที่จะเกิดขึ้น:

  1. คุณส่งแพ็กเก็ต SYN ไปที่ example.com
  2. example.com ตอบกลับด้วย SYN ACK (จนถึงตอนนี้ดีมาก!)
  3. เคอร์เนล Linux บนเครื่องของคุณได้รับ SYN ACK คิดว่า “wtf?? ฉันไม่ได้ทำการเชื่อมต่อนี้หรือไม่?” และปิดการเชื่อมต่อ
  4. คุณเศร้า ไม่มีการเชื่อมต่อ TCP สำหรับคุณ

ฉันกำลังคุยกับเพื่อนเกี่ยวกับปัญหานี้เมื่อสองสามปีก่อน และเขาบอกว่า “คุณควรใช้ tun/tap!” ใช้เวลาสองสามชั่วโมงในการหาวิธีทำ นั่นคือเหตุผลที่ฉันเขียนบล็อกโพสต์นี้ 🙂

tun/tap ให้ “อุปกรณ์เครือข่ายเสมือน” แก่คุณ

วิธีที่ฉันชอบนึกถึง tun/tap คือ ลองนึกภาพว่าฉันมีคอมพิวเตอร์เครื่องเล็กๆ ในเครือข่ายซึ่งกำลังส่งและรับแพ็กเก็ตเครือข่าย แต่แทนที่จะเป็นคอมพิวเตอร์จริงๆ มันเป็นแค่โปรแกรม Python ที่ฉันเขียน

คำอธิบายนั้นแย่กว่าที่ฉันต้องการจริงๆ ฉันหวังว่าฉันจะเข้าใจอย่างถ่องแท้ว่าอุปกรณ์ tun/tap เชื่อมต่อกับสแต็กเครือข่าย Linux จริงอย่างไร แต่น่าเสียดายที่ฉันไม่เข้าใจ ดังนั้น “อุปกรณ์เครือข่ายเสมือน” คือสิ่งที่คุณได้รับ หวังว่าตัวอย่างโค้ดด้านล่างจะทำให้ทุกอย่างชัดเจนขึ้นเล็กน้อย

ตุน vs แทป

ระบบที่เรียกว่า “tun/tap” ให้คุณสร้างอินเทอร์เฟซเครือข่ายได้สองประเภท:

  • “tun” ซึ่งให้คุณตั้งค่า IP-layer packets
  • “แตะ” ซึ่งให้คุณตั้งค่าอีเธอร์เน็ตเลเยอร์แพ็คเก็ต

เรากำลังจะใช้ tun เพราะนั่นคือสิ่งที่ฉันสามารถหาวิธีไปทำงานได้ เป็นไปได้ว่าการแตะจะได้ผลเช่นกัน

วิธีสร้างอินเทอร์เฟซ tun

นี่คือวิธีที่ฉันสร้างอินเทอร์เฟซ tun ด้วยที่อยู่ IP 192.0.2.2

 sudo ip tuntap add name tun0 mode tun user $USER sudo ip link set tun0 up sudo ip addr add 192.0.2.1 peer 192.0.2.2 dev tun0 sudo iptables -t nat -A POSTROUTING -s 192.0.2.2 -j MASQUERADE sudo iptables -A FORWARD -i tun0 -s 192.0.2.2 -j ACCEPT sudo iptables -A FORWARD -o tun0 -d 192.0.2.2 -j ACCEPT

คำสั่งเหล่านี้ทำสองสิ่ง:

  1. สร้างอุปกรณ์ปรับ 192.0.2.2 tun และให้สิทธิ์ผู้ใช้ของคุณในการเขียนลงไป)
  2. ตั้งค่า iptables เป็นพร็อกซีแพ็กเก็ตจากอุปกรณ์ tun นั้นไปยังอินเทอร์เน็ตโดยใช้ NAT

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

ฉันจะไม่อธิบายคำสั่ง ip addr add นี้เพราะฉันไม่เข้าใจมัน ฉันพบว่า ip นั้นไม่สามารถเข้าใจได้มากและตอนนี้ฉันลาออกจากการคัดลอกและวางคำสั่ง ip โดยไม่เข้าใจอย่างถ่องแท้ มันใช้งานได้แม้ว่า

วิธีเชื่อมต่อกับอินเทอร์เฟซ tun ใน Python

นี่คือฟังก์ชั่นในการเปิดอินเทอร์เฟซ tun คุณเรียกมันว่า openTun('tun0') ฉันค้นพบวิธีเขียนมันโดยค้นหาคำว่า “tun” ผ่านซอร์สโค้ด scapy

 import struct from fcntl import ioctl def openTun(tunName): tun = open("/dev/net/tun", "r+b", buffering=0) LINUX_IFF_TUN = 0x0001 LINUX_IFF_NO_PI = 0x1000 LINUX_TUNSETIFF = 0x400454CA flags = LINUX_IFF_TUN | LINUX_IFF_NO_PI ifs = struct.pack("16sH22s", tunName, flags, b"") ioctl(tun, LINUX_TUNSETIFF, ifs) return tun

ทั้งหมดนี้กำลังทำอยู่

  1. เปิด /dev/net/tun ในโหมดไบนารี
  2. เรียก ioctl เพื่อบอก Linux ว่าเราต้องการอุปกรณ์ tun และตัวที่เราต้องการเรียกว่า tun0 (หรือ tunName ใดก็ตามที่เราส่งไปยังฟังก์ชัน)

เมื่อเปิดแล้ว เราสามารถ read และ write ได้เหมือนกับไฟล์อื่นๆ ใน Python

มาส่งแพ็กเก็ต SYN กันเถอะ!

ตอนนี้เรามีฟังก์ชัน openTun แล้ว เราก็สามารถส่งแพ็กเก็ต SYN ได้!

นี่คือลักษณะของโค้ด Python โดยใช้ฟังก์ชัน openTun

 syn = b'E\x00\x00,\x00\x01\x00\[email protected]\x06\x00\xc4\xc0\x00\x02\x02"\xc2\x95Cx\x0c\x00P\xf4p\x98\x8b\x00\x00\x00\x00`\x02\xff\xff\x18\xc6\x00\x00\x02\x04\x05\xb4' tun = openTun(b"tun0") tun.write(syn) reply = tun.read(1024) print(repr(reply))

ถ้าฉันเรียกใช้สิ่งนี้เป็น sudo python3 syn.py มันจะพิมพ์คำตอบจาก example.com :

 b'E\x00\x00,\x00\[email protected]\x00&\x06\xda\xc4"\xc2\x95C\xc0\x00\x02\x02\x00Px\x0cyvL\x84\xf4p\x98\x8c`\x12\xfb\xe0W\xb5\x00\x00\x02\x04\x04\xd8'

เห็นได้ชัดว่านี่เป็นวิธีที่งี่เง่ามากในการส่งแพ็กเก็ต SYN – การใช้งานจริงจะมีโค้ดจริงเพื่อสร้างสตริงไบต์นั้นแทนฮาร์ดโค้ด และเราจะแยกวิเคราะห์การตอบกลับแทนที่จะพิมพ์สตริงไบต์ดิบ แต่ฉันไม่ต้องการเข้าไปในโครงสร้างของ TCP ในโพสต์นี้ นั่นคือสิ่งที่เรากำลังทำอยู่

ดูแพ็กเก็ตเหล่านี้ด้วย tcpdump

หากเราเรียกใช้ tcpdump บนอินเทอร์เฟซ tun0 เราจะเห็นแพ็กเก็ตที่เราส่งและคำตอบจาก example.com :

 $ sudo tcpdump -ni tun0 12:51:01.905933 IP 192.0.2.2.30732 > 34.194.149.67.80: Flags [S], seq 4101019787, win 65535, options [mss 1460], length 0 12:51:01.932178 IP 34.194.149.67.80 > 192.0.2.2.30732: Flags [S.], seq 3300937416, ack 4101019788, win 64480, options [mss 1240], length 0

Flags [S] คือ SYN ที่เราส่ง และ Flags [S.] คือแพ็กเก็ต SYN ACK ในการตอบกลับ! เราสื่อสารสำเร็จแล้ว! และสแต็กเครือข่าย Linux ก็ไม่รบกวนเลย!

tcpdump ยังแสดงให้เราเห็นว่า NAT ทำงานอย่างไร

นอกจากนี้เรายังสามารถเรียกใช้ tcpdump บนอินเทอร์เฟซเครือข่ายจริงของฉัน ( wlp3so การ์ดไร้สายของฉัน) เพื่อดูการส่งและรับแพ็กเก็ต เราจะผ่าน -i wlp3s0 แทน -i tun0

 $ sudo tcpdump -ni wlp3s0 host 34.194.149.67 tcpdump: verbose output suppressed, use -v[v]... for full protocol decode listening on wlp3s0, link-type EN10MB (Ethernet), snapshot length 262144 bytes 12:56:01.204382 IP 192.168.1.181.30732 > 34.194.149.67.80: Flags [S], seq 4101019787, win 65535, options [mss 1460], length 0 12:56:01.228239 IP 34.194.149.67.80 > 192.168.1.181.30732: Flags [S.], seq 144769955, ack 4101019788, win 64480, options [mss 1240], length 0 12:56:05.334427 IP 34.194.149.67.80 > 192.168.1.181.30732: Flags [S.], seq 144769955, ack 4101019788, win 64480, options [mss 1240], length 0 12:56:13.524973 IP 34.194.149.67.80 > 192.168.1.181.30732: Flags [S.], seq 144769955, ack 4101019788, win 64480, options [mss 1240], length 0 12:56:29.705007 IP 34.194.149.67.80 > 192.168.1.181.30732: Flags [S.], seq 144769955, ack 4101019788, win 64480, options [mss 1240], length 0

สองสามสิ่งที่ควรสังเกตที่นี่:

  • ที่อยู่ IP นั้นแตกต่างกัน – กฎ IPtables จากด้านบนได้เขียนใหม่จาก 192.0.2.2 เป็น 192.168.1.181 การเขียนใหม่นี้เรียกว่า “การแปลที่อยู่เครือข่าย” หรือ “NAT”
  • เราได้รับคำตอบมากมายจาก example.com ซึ่งกำลังดำเนินการย้อนกลับแบบเอ็กซ์โพเนนเชียล ซึ่งจะลองใหม่อีกครั้งหลังจาก 4 วินาที จากนั้น 8 วินาที จากนั้น 16 วินาที นั่นเป็นเพราะว่าเราไม่ได้ทำ TCP handshake ให้เสร็จ เราแค่ส่ง SYN แล้วปล่อยให้มันค้าง! จริงๆ แล้วมีการโจมตี DDOS ประเภทหนึ่งที่เรียกว่า SYN flooding แต่เพียงแค่ส่งหนึ่งหรือสองแพ็กเก็ต SYN ก็ไม่ใช่เรื่องใหญ่
  • ฉันต้องเพิ่ม host 34.194.149.67 เนื่องจากมีการส่งแพ็กเก็ต TCP จำนวนมากในการเชื่อมต่อ wifi จริงของฉัน ดังนั้นฉันจึงไม่ต้องสนใจสิ่งเหล่านั้น

ฉันไม่แน่ใจเลยว่าทำไมเราเห็น SYN ตอบกลับใน wlp3s0 มากกว่า tun0 ฉันเดาว่าเป็นเพราะเราอ่าน 1 คำตอบในโปรแกรม Python ของเราเท่านั้น

มันค่อนข้างง่ายและน่าเชื่อถือจริงๆ

ครั้งสุดท้ายที่ฉันพยายามใช้ TCP ใน Python ฉันทำกับสิ่งที่เรียกว่า “การปลอมแปลง ARP” ฉันจะไม่พูดถึงเรื่องนี้ที่นี่ (มีโพสต์เกี่ยวกับบล็อกนี้ในปี 2013) แต่วิธีนี้น่าเชื่อถือกว่ามาก

และการปลอมแปลง ARP เป็นเรื่องคร่าวๆ ที่ต้องทำบนเครือข่ายที่คุณไม่ได้เป็นเจ้าของ

นี่คือรหัส

ฉันใส่รหัสทั้งหมดจากโพสต์บล็อก นี้ในส่วนสำคัญนี้ ถ้าคุณต้องการลองด้วยตัวเอง คุณสามารถเรียกใช้

 bash setup.sh # needs to run as root, has lots of `sudo` commands python3 syn.py # runs as a regular user

ใช้งานได้บน Linux เท่านั้น แต่ฉันคิดว่ามีวิธีตั้งค่า tun/tap บน Mac ด้วย

ปลั๊กสำหรับ scapy

ฉันจะปิดด้วยปลั๊กสำหรับ scapy ที่นี่: เป็นไลบรารีเครือข่าย Python ที่ยอดเยี่ยมมากสำหรับการทดลองประเภทนี้โดยไม่ต้องเขียนโค้ดทั้งหมดด้วยตัวเอง

โพสต์นี้เกี่ยวกับการเขียนโค้ดทั้งหมดด้วยตัวเอง ดังนั้นฉันจะไม่พูดอะไรมากไปกว่านี้

เซิร์ฟเวอร์ล็อกอินระยะไกลของเล่น

สวัสดี! วันก่อน เราได้พูดคุยเกี่ยวกับ สิ่งที่เกิดขึ้นเมื่อคุณกดปุ่มในเทอร์มินัลของคุณ

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

เป้าหมาย: “ssh” ไปยังคอมพิวเตอร์ระยะไกล

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

ความแตกต่างที่ใหญ่ที่สุดระหว่างโปรแกรมนี้กับ SSH ก็คือไม่มีการรักษาความปลอดภัยอย่างแท้จริง (ไม่ใช่แม้แต่รหัสผ่าน) – ใครก็ตามที่สามารถเชื่อมต่อ TCP กับเซิร์ฟเวอร์สามารถรับเชลล์และรันคำสั่งได้

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

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

มาเริ่มกันที่เซิร์ฟเวอร์กัน!

เราจะเขียนไคลเอนต์ด้วย แต่เซิร์ฟเวอร์เป็นส่วนที่น่าสนใจ ดังนั้นมาเริ่มกันที่ เราจะเขียนเซิร์ฟเวอร์ที่รับฟังพอร์ต TCP (ฉันเลือก 7777) และสร้างเทอร์มินัลระยะไกลสำหรับลูกค้าที่เชื่อมต่อเพื่อใช้งาน

เมื่อเซิร์ฟเวอร์ได้รับการเชื่อมต่อใหม่ จะต้อง:

  1. สร้าง pseudoterminal ให้ลูกค้าใช้
  2. เริ่มกระบวนการ bash shell เพื่อให้ไคลเอนต์ใช้
  3. เชื่อมต่อ bash กับ pseudoterminal
  4. คัดลอกข้อมูลไปมาอย่างต่อเนื่องระหว่างการเชื่อมต่อ TCP และ pseudoterminal

ฉันเพิ่งพูดคำว่า “pseudoterminal” ไปบ่อย ๆ ดังนั้นเรามาพูดถึงความหมายกัน

เทอร์มินัลเทียมคืออะไร

เอาล่ะ เทอร์มินัลเทียมคืออะไร?

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

โดยพื้นฐานแล้วแนวคิดก็คือด้านหนึ่ง เรามีการเชื่อมต่อ TCP และอีกด้านหนึ่ง เรามี bash shell ดังนั้นเราจึงต้องขอส่วนหนึ่งของ pseudoterminal กับการเชื่อมต่อ TCP และปลายอีกด้านหนึ่งเพื่อทุบตี

สองส่วนของ pseudoterminal เรียกว่า:

  • “ต้นแบบเทอร์มินัลเทียม” นี่คือจุดสิ้นสุดที่เราจะเชื่อมต่อกับการเชื่อมต่อ TCP
  • “อุปกรณ์เทอร์มินัลเทียมทาส” เราจะตั้งค่า stdout , stderr และ stdin ของ bash shell เป็นสิ่งนี้

เมื่อเชื่อมต่อแล้ว เราสามารถสื่อสารกับ bash ผ่านการเชื่อมต่อ TCP และเราจะมีรีโมตเชลล์!

ทำไมเราถึงต้องการสิ่ง “pseudoterminal” นี้อยู่แล้ว?

คุณอาจกำลังสงสัย – จูเลีย ถ้าเทอร์มินัลปลอมเป็นเหมือนซ็อกเก็ต ทำไมเราไม่สามารถตั้งค่า stdout / stderr / stdin ของเชลล์ทุบตีเป็นซ็อกเก็ต TCP ได้

และคุณสามารถ! เราสามารถเขียนตัวจัดการการเชื่อมต่อ TCP แบบนี้ที่ทำอย่างนั้นได้ ไม่ใช่โค้ดจำนวนมาก ( server-notty.go )

 func handle(conn net.Conn) { tty, _ := conn.(*net.TCPConn).File() // start bash with tcp connection as stdin/stdout/stderr cmd := exec.Command("bash") cmd.Stdin = tty cmd.Stdout = tty cmd.Stderr = tty cmd.Start() }

มันยังใช้งานได้ – ถ้าเราเชื่อมต่อกับ nc localhost 7778 เราสามารถเรียกใช้คำสั่งและดูผลลัพธ์ได้

แต่มีปัญหาเล็กน้อย ฉันจะไม่แสดงรายการทั้งหมด แค่สองรายการ

ปัญหาที่ 1: Ctrl + C ไม่ทำงาน

วิธีการทำงานของ Ctrl + C ในเซสชันการเข้าสู่ระบบระยะไกลคือ

  • คุณกด ctrl + c
  • ที่ได้รับการแปลเป็น 0x03 และส่งผ่านการเชื่อมต่อ TCP
  • เครื่องปลายทางรับ
  • เคอร์เนลลินุกซ์ในส่วนอื่น ๆ “เฮ้นั่นคือ Ctrl + C!”
  • Linux ส่ง SIGINT ไปยังกระบวนการที่เหมาะสม (เพิ่มเติมเกี่ยวกับ “กระบวนการที่เหมาะสม” ในภายหลัง)

หาก “เทอร์มินัล” เป็นเพียงการเชื่อมต่อ TCP สิ่งนี้จะไม่ทำงาน เพราะเมื่อคุณส่ง 0x04 ไปยังการเชื่อมต่อ TCP ลินุกซ์จะไม่ส่ง SIGINT ไปยังกระบวนการใดๆ อย่างน่าอัศจรรย์

ปัญหาที่ 2: top ไม่ทำงาน

เมื่อฉันพยายามเรียกใช้ top ในเชลล์นี้ ฉันได้รับข้อความแสดงข้อผิดพลาด top: failed tty get หากเราลากเส้น เราจะเห็นการเรียกระบบนี้:

 ioctl(2, TCGETS, 0x7ffec4e68d60) = -1 ENOTTY (Inappropriate ioctl for device)

ดังนั้น top กำลังเรียกใช้ ioctl บนตัวอธิบายไฟล์เอาต์พุต (2) เพื่อรับข้อมูลบางอย่างเกี่ยวกับเทอร์มินัล แต่ลีนุกซ์ก็แบบว่า “เฮ้ นี่ไม่ใช่เทอร์มินัล!” และส่งคืนข้อผิดพลาด

มีหลายอย่างผิดพลาด แต่หวังว่า ณ จุดนี้คุณจะมั่นใจว่าเราจำเป็นต้องตั้งค่า stdout/stderr ของ bash ให้เป็นเทอร์มินัล ไม่ใช่อย่างอื่นเช่นซ็อกเก็ต

เรามาเริ่มดูที่รหัสเซิร์ฟเวอร์และดูว่าการสร้าง pseudoterminal เป็นอย่างไร

ขั้นตอนที่ 1: สร้าง pseudoterminal

นี่คือโค้ด Go บางส่วนเพื่อสร้าง pseudoterminal บน Linux สิ่งนี้ถูกคัดลอกจาก github.com/creack/pty แต่ฉันได้ลบการจัดการข้อผิดพลาดบางส่วนเพื่อให้ตรรกะง่ายต่อการติดตามเล็กน้อย:

 pty, _ := os.OpenFile("/dev/ptmx", os.O_RDWR, 0) sname := ptsname(p) unlockpt(p) tty, _ := os.OpenFile(sname, os.O_RDWR|syscall.O_NOCTTY, 0)

ในภาษาอังกฤษ สิ่งที่เราทำคือ:

  • เปิด /dev/ptmx เพื่อรับ “pseudoterminal master” อีกครั้งนั่นคือส่วนที่เราจะเชื่อมต่อกับการเชื่อมต่อ TCP
  • รับชื่อไฟล์ของ “slave pseudoterminal device” ซึ่งจะเป็น /dev/pts/13 หรือบางอย่าง
  • “ปลดล็อค” pseudoterminal เพื่อให้เราใช้งานได้ ฉันไม่รู้ว่ามันคืออะไร (ทำไมมันถึงล็อคตั้งแต่เริ่มต้น?) แต่คุณต้องทำด้วยเหตุผลบางอย่าง
  • open /dev/pts/13 (หรือหมายเลขใดก็ตามที่เราได้รับจาก ptsname ) เพื่อรับ “slave pseudoterminal device”

ฟังก์ชั่น unlockpt และ ptsname เหล่านั้นทำอะไรได้บ้าง? พวกเขาเพียงแค่ทำการเรียกระบบ ioctl ไปยังเคอร์เนลลินุกซ์ การสื่อสารทั้งหมดกับเคอร์เนล Linux เกี่ยวกับเทอร์มินัลดูเหมือนจะผ่านการเรียกระบบ ioctl ต่างๆ

นี่คือรหัส มันค่อนข้างสั้น: (ฉันเพิ่งคัดลอกมาจาก creack/pty อีกครั้ง)

 func ptsname(f *os.File) string { var n uint32 ioctl(f.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n))) return "/dev/pts/" + strconv.Itoa(int(n)) } func unlockpt(f *os.File) { var u int32 // use TIOCSPTLCK with a pointer to zero to clear the lock ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u))) }

ขั้นตอนที่ 2: ขอ pseudoterminal ขึ้นไป bash

สิ่งต่อไปที่เราต้องทำคือเชื่อมต่อ pseudoterminal กับ bash โชคดีที่ง่ายมาก นี่คือรหัส Go สำหรับมัน! เราแค่ต้องเริ่มกระบวนการใหม่และตั้งค่า stdin, stdout และ stderr เป็น tty

 cmd := exec.Command("bash") cmd.Stdin = tty cmd.Stdout = tty cmd.Stderr = tty cmd.SysProcAttr = &syscall.SysProcAttr{ Setsid: true, } cmd.Start()

ง่าย! แม้ว่า – ทำไมเราต้องการ Setsid: true คุณอาจถาม? ฉันพยายามแสดงความคิดเห็นโค้ดนั้นเพื่อดูว่ามีอะไรผิดพลาด ปรากฎว่าสิ่งที่ผิดพลาดคือ – Ctrl + C ใช้งานไม่ได้อีกต่อไป!

Setsid: true สร้าง เซสชัน ใหม่สำหรับกระบวนการทุบตีใหม่ แต่ทำไมถึงทำให้ Ctrl + C ใช้งานได้ Linux รู้ได้อย่างไรว่ากระบวนการใดที่จะส่ง SIGINT ไปเมื่อคุณกด Ctrl + C และเกี่ยวข้องกับเซสชันอย่างไร

Linux รู้ได้อย่างไรว่าจะส่ง Ctrl + C ไปที่กระบวนการใด

ฉันพบว่ามันค่อนข้างสับสน ดังนั้นฉันจึงไปหาหนังสือเล่มโปรดเพื่อเรียนรู้เกี่ยวกับสิ่งนี้: อินเทอร์เฟซการเขียนโปรแกรม linux โดยเฉพาะบทที่ 34 เกี่ยวกับกลุ่มกระบวนการและเซสชัน

บทนั้นมีข้อเท็จจริงสำคัญบางประการ: (#3, #4 และ #5 เป็นคำพูดโดยตรงจากหนังสือ)

  1. ทุกกระบวนการมี รหัสเซสชันและรหัส กลุ่มกระบวนการ (ซึ่งอาจหรืออาจไม่เหมือนกับ PID)
  2. เซสชันประกอบด้วยกลุ่มกระบวนการหลายกลุ่ม
  3. กระบวนการทั้งหมดในเซสชันใช้เทอร์มินัลควบคุมเดียว
  4. เทอร์มินัลอาจเป็นเทอร์มินัลควบคุมได้ไม่เกินหนึ่งเซสชัน
  5. ในช่วงเวลาใดๆ กลุ่มกระบวนการหนึ่งในเซสชันคือ กลุ่มกระบวนการเบื้องหน้า สำหรับเทอร์มินัล และกลุ่มอื่นคือกลุ่มกระบวนการเบื้องหลัง
  6. เมื่อคุณกด Ctrl+C ในเทอร์มินัล SIGINT จะถูกส่งไปยังกระบวนการทั้งหมดในกลุ่มกระบวนการเบื้องหน้า

กลุ่มกระบวนการคืออะไร? ความเข้าใจของฉันคือ:

  • กระบวนการในไพพ์เดียวกัน x | y | z อยู่ในกลุ่มกระบวนการเดียวกัน
  • กระบวนการที่คุณเริ่มต้นบนเชลล์ไลน์เดียวกัน ( x && y && z ) อยู่ในกลุ่มกระบวนการเดียวกัน
  • กระบวนการลูกเป็นค่าเริ่มต้นในกลุ่มกระบวนการเดียวกัน เว้นแต่คุณจะตัดสินใจเป็นอย่างอื่นอย่างชัดเจน

ฉันไม่รู้สิ่งนี้ส่วนใหญ่ (ฉันไม่รู้ว่ากระบวนการมี ID เซสชัน!) ดังนั้นนี่เป็นเรื่องที่ต้องสนใจมาก ฉันพยายามวาดไดอะแกรมศิลปะ ASCII คร่าวๆ ของสถานการณ์

 (maybe) terminal --- session --- process group --- process | |- process | |- process |- process group | |- process group

ดังนั้นเมื่อเรากด Ctrl+C ในเทอร์มินัล นี่คือสิ่งที่ฉันคิดว่าเกิดขึ้น:

  • \x04 ถูกเขียนไปยัง “pseudotermimal master” ของเทอร์มินัล
  • Linux ค้นหา เซสชัน สำหรับเทอร์มินัลนั้น (ถ้ามี)
  • Linux ค้นหา กลุ่มกระบวนการเบื้องหน้า สำหรับเซสชันนั้น
  • Linux ส่ง SIGINT

หากเราไม่สร้างเซสชันใหม่สำหรับกระบวนการทุบตีใหม่ของเรา เทอร์มินัลเทียมใหม่ของเราจะ ไม่มี เซสชันที่เกี่ยวข้อง ดังนั้นจึงไม่มีอะไรเกิดขึ้นเมื่อเรากด Ctrl+C แต่ถ้าเราสร้างเซสชันใหม่ pseudoterminal ใหม่จะมีเซสชันใหม่ที่เกี่ยวข้องด้วย

วิธีรับรายการเซสชันทั้งหมดของคุณ

นอกจากนี้ หากคุณต้องการรับรายการเซสชันทั้งหมดบนเครื่อง Linux ของคุณ โดยจัดกลุ่มตามเซสชัน คุณสามารถเรียกใช้:

 $ ps -eo user,pid,pgid,sess,cmd | sort -k3

ซึ่งรวมถึง PID, ID กลุ่มกระบวนการ และ ID เซสชัน ตัวอย่างของผลลัพธ์ ต่อไปนี้คือสองกระบวนการในไปป์ไลน์:

 bork 58080 58080 57922 ps -eo user,pid,pgid,sess,cmd bork 58081 58080 57922 sort -k3

คุณจะเห็นว่าพวกเขาแชร์ ID กลุ่มกระบวนการและ ID เซสชันเดียวกัน แต่แน่นอนว่าพวกเขามี PID ต่างกัน

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

ขั้นตอนที่ 3: กำหนดขนาดหน้าต่าง

ต้องบอกเทอร์มินอลว่าใหญ่แค่ไหน!

อีกครั้งฉันเพิ่งคัดลอกมาจาก creack/pty ฉันตัดสินใจฮาร์ดโค้ดขนาดเป็น 80×24

 Setsize(tty, &Winsize{ Cols: 80, Rows: 24, })

เช่นเดียวกับการรับชื่อไฟล์ pts ของเทอร์มินัลและปลดล็อก การตั้งค่าขนาดเป็นเพียงการเรียกระบบ ioctl หนึ่งครั้ง:

 func Setsize(t *os.File, ws *Winsize) { ioctl(t.Fd(), syscall.TIOCSWINSZ, uintptr(unsafe.Pointer(ws))) }

ค่อนข้างง่าย! เราสามารถทำอะไรที่ฉลาดกว่านี้และได้ขนาดหน้าต่างจริง แต่ฉันขี้เกียจเกินไป

ขั้นตอนที่ 4: คัดลอกข้อมูลระหว่างการเชื่อมต่อ TCP และ pseudoterminal

เพื่อเป็นการเตือนความจำ ขั้นตอนคร่าวๆ ของเราในการตั้งค่าเซิร์ฟเวอร์การเข้าสู่ระบบระยะไกลนี้คือ:

  1. สร้าง pseudoterminal ให้ลูกค้าใช้
  2. เริ่มกระบวนการ bash shell
  3. เชื่อมต่อ bash กับ pseudoterminal
  4. คัดลอกข้อมูลไปมาอย่างต่อเนื่องระหว่างการเชื่อมต่อ TCP และ pseudoterminal

เราได้ทำ 1, 2 และ 3 แล้ว ตอนนี้เราแค่ต้องการส่งข้อมูลระหว่างการเชื่อมต่อ TCP และ pseudoterminal

มีการเรียก io.Copy สองครั้ง การโทรหนึ่งครั้งเพื่อคัดลอกอินพุต จาก การเชื่อมต่อ tcp และอีกรายการหนึ่งเพื่อคัดลอกเอาต์พุต ไปยัง การเชื่อมต่อ TCP นี่คือลักษณะของรหัส:

 go func() { io.Copy(pty, conn) }() io.Copy(conn, pty)

อันแรกอยู่ใน goroutine เพื่อให้ทั้งคู่วิ่งขนานกัน

ค่อนข้างง่าย!

ขั้นตอนที่ 5: ออกเมื่อเราทำเสร็จแล้ว

ฉันยังเพิ่มโค้ดเล็กน้อยเพื่อปิดการเชื่อมต่อ TCP เมื่อคำสั่งออก

 go func() { cmd.Wait() conn.Close() }()

และนั่นคือทั้งหมดสำหรับเซิร์ฟเวอร์! คุณสามารถดูรหัส Go ทั้งหมดได้ที่นี่: server.go

ถัดไป: เขียนลูกค้า

ต่อไปเราต้องเขียนลูกค้า นี่เป็นมากกว่าเซิร์ฟเวอร์เพราะเราไม่ต้องตั้งค่าเทอร์มินัลมากนัก มีเพียง 3 ขั้นตอน:

  1. ใส่เทอร์มินัลลงในโหมดดิบ
  2. คัดลอก stdin/stdout ไปยังการเชื่อมต่อ TCP
  3. รีเซ็ตเทอร์มินัล

ไคลเอ็นต์ขั้นตอนที่ 1: วางเทอร์มินัลลงในโหมด “ดิบ”

เราจำเป็นต้องตั้งค่าเทอร์มินัลไคลเอ็นต์ให้อยู่ในโหมด “ดิบ” เพื่อให้ทุกครั้งที่คุณกดปุ่ม คีย์จะถูกส่งไปยังการเชื่อมต่อ TCP ทันที หากเราไม่ทำเช่นนี้ ทุกอย่างจะถูกส่งเมื่อคุณกด Enter

“โหมด Raw” ไม่ใช่สิ่งเดียว แต่เป็นแฟล็กจำนวนมากที่คุณต้องการปิด มีบทช่วยสอนที่ดีที่อธิบายแฟล็กทั้งหมดที่เราต้องปิดที่เรียกว่า Entering raw mode

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

ฉันหาวิธีทำสิ่งนี้ใน Go โดยไปที่ https://grep.app แล้วพิมพ์ syscall.TCSETS เพื่อค้นหาโค้ด Go อื่นที่ทำสิ่งเดียวกัน

 func MakeRaw(fd uintptr) syscall.Termios { // from https://github.com/getlantern/lantern/blob/devel/archive/src/golang.org/x/crypto/ssh/terminal/util.go var oldState syscall.Termios ioctl(fd, syscall.TCGETS, uintptr(unsafe.Pointer(&oldState))) newState := oldState newState.Iflag &^= syscall.ISTRIP | syscall.INLCR | syscall.ICRNL | syscall.IGNCR | syscall.IXON | syscall.IXOFF newState.Lflag &^= syscall.ECHO | syscall.ICANON | syscall.ISIG ioctl(fd, syscall.TCSETS, uintptr(unsafe.Pointer(&newState))) return oldState }

ไคลเอ็นต์ขั้นตอนที่ 2: คัดลอก stdin/stdout ไปยังการเชื่อมต่อ TCP

นี้เหมือนกับสิ่งที่เราทำกับเซิร์ฟเวอร์ มันเป็นรหัสที่น้อยมาก:

 go func() { io.Copy(conn, os.Stdin) }() io.Copy(os.Stdout, conn)

ไคลเอ็นต์ขั้นตอนที่ 3: คืนค่าสถานะของเทอร์มินัล

เราสามารถใส่เทอร์มินัลกลับเข้าสู่โหมดที่เริ่มต้นในลักษณะนี้ (อีก ioctl !):

 func Restore(fd uintptr, oldState syscall.Termios) { ioctl(fd, syscall.TCSETS, uintptr(unsafe.Pointer(&oldState))) }

เราทำได้!

เราได้เขียนเซิร์ฟเวอร์การเข้าสู่ระบบระยะไกลขนาดเล็กที่ช่วยให้ทุกคนเข้าสู่ระบบได้! ไชโย!

เห็นได้ชัดว่าสิ่งนี้ไม่มีความปลอดภัย ดังนั้นฉันจะไม่พูดถึงแง่มุมนั้น

มันทำงานบนอินเทอร์เน็ตสาธารณะ! คุณสามารถลอง!

ประมาณสัปดาห์หน้า ฉันจะสาธิตสิ่งนี้บนอินเทอร์เน็ตที่ tetris.jvns.ca มันรัน tetris แทนที่จะเป็นเชลล์เพราะฉันต้องการหลีกเลี่ยงการละเมิด แต่ถ้าคุณต้องการลองใช้กับเชลล์ คุณสามารถรันบนคอมพิวเตอร์ของคุณเองได้ 🙂

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

 stty raw -echo && nc tetris.jvns.ca 7777 && stty sane

นี้จะช่วยให้คุณเล่นเกมเทตริสเทอร์มินัลที่เรียกว่า tint

คุณยังสามารถใช้ โปรแกรม client.go และเรียกใช้ go run client.go tetris.jvns.ca 7777

นี่ไม่ใช่โปรโตคอลที่ดี

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

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

มันจะทำให้เทอร์มินัลของคุณเลอะเล็กน้อย

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

หากเราต้องการแก้ไขปัญหานี้จริง เราจะต้องคืนค่าขนาดหน้าต่างหลังจากที่เราทำเสร็จแล้ว แต่เราจำเป็นต้องมีโปรโตคอลจริงมากกว่า “แค่คัดลอกไบต์ไปมาด้วย TCP สุ่มสี่สุ่มห้า” และฉันไม่ได้ ไม่รู้สึกอยากทำอย่างนั้น

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

โครงการเล็กๆ อื่นๆ

นั่นคือทั้งหมด! มีการใช้งานของเล่นที่คล้ายกันอื่น ๆ สองสามโปรแกรมที่ฉันเขียนไว้ที่นี่:

จะเกิดอะไรขึ้นเมื่อคุณกดปุ่มในเทอร์มินัลของคุณ?

ฉันสับสนเกี่ยวกับสิ่งที่เกิดขึ้นกับเทอร์มินัลมาเป็นเวลานาน

แต่สัปดาห์ที่ผ่านมานี้ ฉันใช้ xterm.js เพื่อแสดงเทอร์มินัลแบบโต้ตอบในเบราว์เซอร์ และในที่สุดฉันก็คิดว่าจะถามคำถามพื้นฐานที่ดี: เมื่อคุณกดปุ่มบนแป้นพิมพ์ในเทอร์มินัล (เช่น Delete หรือ Escape หรือ a ) ไบต์ใดที่ส่ง

ตามปกติเราจะตอบคำถามนั้นโดยทำการทดลองและดูว่าเกิดอะไรขึ้น 🙂

ขั้วต่อระยะไกลเป็นเทคโนโลยีที่เก่ามาก

ก่อนอื่น ฉันต้องการจะบอกว่าการแสดงเทอร์มินัลในเบราว์เซอร์ด้วย xterm.js อาจดูเหมือนเป็นสิ่งใหม่ แต่จริงๆ แล้วไม่ใช่ ในยุค 70 คอมพิวเตอร์มีราคาแพง พนักงานจำนวนมากในสถาบันแห่งหนึ่งใช้คอมพิวเตอร์เครื่องเดียวร่วมกัน และแต่ละคนสามารถมี “เทอร์มินัล” ของตนเองกับคอมพิวเตอร์เครื่องนั้นได้

ตัวอย่างเช่น นี่คือภาพถ่ายของเทอร์มินัล VT100 จากยุค 70 หรือ 80 ดูเหมือนว่าอาจเป็นคอมพิวเตอร์ (ค่อนข้างใหญ่!) แต่ก็ไม่ใช่ เพียงแสดงข้อมูลใดก็ตามที่คอมพิวเตอร์จริงส่งมาให้

ขั้วต่อ DEC VT100

แน่นอนว่าในยุค 70 พวกเขาไม่ได้ใช้ websockets สำหรับสิ่งนี้ แต่ข้อมูลที่ถูกส่งกลับไปกลับมานั้นเหมือนเดิมไม่มากก็น้อย

(เทอร์มินัลในรูปนั้นมาจาก Living Computer Museum ในซีแอตเทิล ซึ่งฉันเคยไปเยี่ยมชมครั้งหนึ่ง และเขียน FizzBuzz ในภาษา ed บนระบบ Unix ที่เก่ามาก เป็นไปได้ว่าฉันเคยใช้เครื่องนั้นจริง ๆ หรือหนึ่งในพี่น้องของมัน! ฉัน หวังว่า Living Computer Museum จะเปิดขึ้นอีกครั้ง เป็นการดีที่จะได้เล่นกับคอมพิวเตอร์เครื่องเก่า)

ข้อมูลอะไรถูกส่ง?

เห็นได้ชัดว่าหากคุณต้องการเชื่อมต่อกับคอมพิวเตอร์ระยะไกล (ด้วย ssh หรือใช้ xterm.js และ websocket หรืออย่างอื่น) จำเป็นต้องส่งข้อมูลบางอย่างระหว่างไคลเอนต์และเซิร์ฟเวอร์

โดยเฉพาะ:

  • ลูกค้า ต้องส่งการกดแป้นที่ผู้ใช้พิมพ์ (เช่น ls -l )
  • เซิร์ฟเวอร์ ต้องบอกลูกค้าว่าจะแสดงอะไรบนหน้าจอ

มาดูโปรแกรมจริงที่รันเทอร์มินัลระยะไกลในเบราว์เซอร์และดูว่าข้อมูลใดบ้างที่ถูกส่งกลับไปกลับมา!

เราจะใช้ goterm เพื่อทดลอง

ฉันพบโปรแกรมเล็กๆ นี้บน GitHub ชื่อ goterm ที่รันเซิร์ฟเวอร์ Go ที่ให้คุณโต้ตอบกับเทอร์มินัลในเบราว์เซอร์ได้โดยใช้ xterm.js โปรแกรมนี้ไม่ปลอดภัยมาก แต่ใช้งานได้ง่ายและยอดเยี่ยมสำหรับการเรียนรู้

ฉัน แยกมัน เพื่อให้ใช้งานได้กับ xterm.js ล่าสุด เนื่องจากอัปเดตล่าสุดเมื่อ 6 ปีที่แล้ว จากนั้นฉันได้เพิ่มคำสั่งการบันทึกเพื่อพิมพ์ทุกครั้งที่มีการส่ง / รับไบต์ผ่าน websocket

มาดูการส่งและรับระหว่างการโต้ตอบกับเทอร์มินัลที่แตกต่างกันเล็กน้อย!

ตัวอย่าง: ls

ก่อนอื่นให้เรียกใช้ ls นี่คือสิ่งที่ฉันเห็นในเทอร์มินัล xterm.js :

 [email protected]:/play$ ls file [email protected]:/play$

และนี่คือสิ่งที่รับส่ง: (ในรหัสของฉัน ฉันบันทึก sent: [bytes] ทุกครั้งที่ไคลเอนต์ส่งไบต์และ recv: [bytes] ทุกครั้งที่ได้รับไบต์จากเซิร์ฟเวอร์)

 sent: "l" recv: "l" sent: "s" recv: "s" sent: "\r" recv: "\r\n\x1b[?2004l\r" recv: "file\r\n" recv: "\x1b[[email protected]:/play$ "

ฉันสังเกตเห็น 3 สิ่งในผลลัพธ์นี้:

  1. Echoing: ลูกค้าส่ง l แล้วได้รับ l ที่ส่งกลับทันที ฉันเดาว่าความคิดที่นี่คือลูกค้าโง่จริงๆ – ไม่รู้ว่าเมื่อฉันพิมพ์ l ฉันต้องการให้ l ถูกสะท้อนกลับไปที่หน้าจอ จะต้องมีการบอกอย่างชัดเจนโดยกระบวนการของเซิร์ฟเวอร์เพื่อแสดง
  2. การขึ้นบรรทัดใหม่: เมื่อฉันกด Enter มันจะส่งสัญลักษณ์ \r (การขึ้นบรรทัดใหม่) ไม่ใช่ \n (ขึ้นบรรทัดใหม่)
  3. ลำดับการหลบหนี: \x1b เป็นอักขระหลีก ASCII ดังนั้น \x1b[?2004h จึงบอกให้เทอร์มินัลแสดงบางอย่างหรืออย่างอื่น ฉันคิดว่านี่เป็นลำดับสี แต่ฉันไม่แน่ใจ เราจะพูดถึงลำดับการหลบหนีเพิ่มเติมอีกเล็กน้อยในภายหลัง

เอาล่ะ เรามาทำสิ่งที่ซับซ้อนกว่านี้กันเล็กน้อย

ตัวอย่าง: Ctrl+C

ต่อไป มาดูกันว่าจะเกิดอะไรขึ้นเมื่อเราขัดจังหวะกระบวนการด้วย Ctrl+C นี่คือสิ่งที่ฉันเห็นในเทอร์มินัลของฉัน:

 [email protected]:/play$ cat ^C [email protected]:/play$

และนี่คือสิ่งที่ลูกค้าส่งและรับ

 sent: "c" recv: "c" sent: "a" recv: "a" sent: "t" recv: "t" sent: "\r" recv: "\r\n\x1b[?2004l\r" sent: "\x03" recv: "^C" recv: "\r\n" recv: "\x1b[?2004h" recv: "[email protected]:/play$ "

เมื่อฉันกด Ctrl+C ลูกค้าส่ง \x03 ถ้าฉันค้นหาตาราง ASCII \x03 คือ “สิ้นสุดข้อความ” ซึ่งดูสมเหตุสมผล ฉันคิดว่ามันเจ๋งมากเพราะฉันเคยสับสนเล็กน้อยเกี่ยวกับวิธีการทำงานของ Ctrl+C – เป็นการดีที่รู้ว่ามันแค่ส่งอักขระ \x03

ฉันเชื่อว่าเหตุผลที่ cat ถูกขัดจังหวะเมื่อเรากด Ctrl+C คือเคอร์เนล Linux บนฝั่งเซิร์ฟเวอร์ได้รับอักขระ \x03 นี้ ตระหนักว่ามันหมายถึง “ขัดจังหวะ” จากนั้นจึงส่ง SIGINT ไปยังกระบวนการที่เป็นเจ้าของกลุ่มกระบวนการของ pseudoterminal . ดังนั้นมันจึงถูกจัดการในเคอร์เนลและไม่ใช่ใน userspace

ตัวอย่าง: Ctrl+D

ลองทำสิ่งเดียวกัน ยกเว้น Ctrl+D นี่คือสิ่งที่ฉันเห็นในเทอร์มินัลของฉัน:

 [email protected]:/play$ cat [email protected]:/play$

และนี่คือสิ่งที่ส่งและรับ:

 sent: "c" recv: "c" sent: "a" recv: "a" sent: "t" recv: "t" sent: "\r" recv: "\r\n\x1b[?2004l\r" sent: "\x04" recv: "\x1b[?2004h" recv: "[email protected]:/play$ "

คล้ายกับ Ctrl+C มาก ยกเว้นว่าส่ง \x04 แทน \x03 เย็น! \x04 สอดคล้องกับ ASCII “สิ้นสุดการส่ง”

แล้ว Ctrl + อีกตัวอักษรล่ะ?

ต่อไปฉันสงสัยเกี่ยวกับ – ถ้าฉันส่ง Ctrl+e ไบต์ใดจะถูกส่งไป

ปรากฎว่ามันเป็นเพียงตัวเลขของตัวอักษรนั้นในตัวอักษรเช่นนี้:

  • Ctrl+a => 1
  • Ctrl+b => 2
  • Ctrl+c => 3
  • Ctrl+d => 4
  • Ctrl+z => 26

นอกจากนี้ Ctrl+Shift+b ยังทำสิ่งเดียวกันกับ Ctrl+b (มันเขียน 0x2 )

แล้วปุ่มอื่นๆ บนคีย์บอร์ดล่ะ? นี่คือสิ่งที่พวกเขาแมปไปยัง:

  • Tab -> 0x9 (เหมือนกับ Ctrl+I เนื่องจากฉันเป็นตัวอักษรตัวที่ 9)
  • หนี -> \x1b
  • Backspace -> \x7f
  • หน้าแรก -> \x1b[H
  • สิ้นสุด: \x1b[F
  • พิมพ์หน้าจอ: \x1b\x5b\x31\x3b\x35\x41
  • แทรก: \x1b\x5b\x32\x7e
  • ลบ -> \x1b\x5b\x33\x7e
  • คีย์ Meta ของฉันไม่ทำอะไรเลย

อัลท์ล่ะ? จากการทดลองของฉัน (และ Googling บางส่วน) ดูเหมือนว่า Alt จะเหมือนกับ “Escape” อย่างแท้จริง เว้นแต่ว่าการกด Alt ด้วยตัวเองจะไม่ส่งอักขระใดๆ ไปยังเทอร์มินัล และการกด Escape ด้วยตัวเองก็ทำได้ ดังนั้น:

  • alt + d => \x1bd (และเหมือนกันทุกตัวอักษร)
  • alt + shift + d => \x1bD (และเหมือนกันทุกตัวอักษร)
  • etcetera

ลองดูอีกตัวอย่างหนึ่ง!

ตัวอย่าง: nano

นี่คือสิ่งที่ส่งและรับเมื่อฉันเรียกใช้โปรแกรมแก้ไขข้อความ nano :

 recv: "\r\x1b[[email protected]:/play$ " sent: "n" [[]byte{0x6e}] recv: "n" sent: "a" [[]byte{0x61}] recv: "a" sent: "n" [[]byte{0x6e}] recv: "n" sent: "o" [[]byte{0x6f}] recv: "o" sent: "\r" [[]byte{0xd}] recv: "\r\n\x1b[?2004l\r" recv: "\x1b[?2004h" recv: "\x1b[?1049h\x1b[22;0;0t\x1b[1;16r\x1b(B\x1b[m\x1b[4l\x1b[?7h\x1b[39;49m\x1b[?1h\x1b=\x1b[?1h\x1b=\x1b[?25l" recv: "\x1b[39;49m\x1b(B\x1b[m\x1b[H\x1b[2J" recv: "\x1b(B\x1b[0;7m GNU nano 6.2 \x1b[44bNew Buffer \x1b[53b \x1b[1;123H\x1b(B\x1b[m\x1b[14;38H\x1b(B\x1b[0;7m[ Welcome to nano. For basic help, type Ctrl+G. ]\x1b(B\x1b[m\r\x1b[15d\x1b(B\x1b[0;7m^G\x1b(B\x1b[m Help\x1b[15;16H\x1b(B\x1b[0;7m^O\x1b(B\x1b[m Write Out \x1b(B\x1b[0;7m^W\x1b(B\x1b[m Where Is \x1b(B\x1b[0;7m^K\x1b(B\x1b[m Cut\x1b[15;61H"

คุณสามารถเห็นข้อความบางส่วนจาก UI ในนั้น เช่น “GNU nano 6.2” และ \x1b[27m สิ่งเหล่านี้เป็น Escape Sequence มาพูดถึงฉากหลบหนีกันสักหน่อย!

ลำดับการหลบหนี ANSI

\x1b[ สิ่งที่อยู่เหนือ vim ที่ส่งไปยังไคลเอนต์เรียกว่า “ลำดับการหลบหนี” หรือ “รหัสหลบหนี” นี่เป็นเพราะพวกเขาทั้งหมดเริ่มต้นด้วย \x1b อักขระ “escape” . เปลี่ยนตำแหน่งของเคอร์เซอร์ ทำให้ข้อความเป็นตัวหนาหรือขีดเส้นใต้ เปลี่ยนสี ฯลฯ Wikipedia มีประวัติบางส่วน หากคุณสนใจ

ยกตัวอย่างง่ายๆ: ถ้าคุณเรียกใช้

 echo -e '\e[0;31mhi\e[0m there'

ในเทอร์มินัลของคุณ มันจะพิมพ์คำว่า “hi there” โดยที่ “hi” เป็นสีแดงและ “there” เป็นสีดำ หน้านี้ มีตัวอย่างที่ดีของ Escape Code สำหรับสีและการจัดรูปแบบ

ฉันคิดว่ามีมาตรฐานที่แตกต่างกันเล็กน้อยสำหรับรหัส Escape แต่ความเข้าใจของฉันคือชุดรหัส Escape ทั่วไปที่ผู้คนใช้บน Unix มาจาก VT100 (เทอร์มินัลเก่านั้นในรูปภาพที่ด้านบนสุดของโพสต์บล็อก) และ ไม่ได้เปลี่ยนแปลงมากนักในช่วง 40 ปีที่ผ่านมา

Escape Code คือสาเหตุที่เทอร์มินัลของคุณอาจเกิดความยุ่งเหยิงได้ หากคุณ cat ไบนารีจำนวนมากมาที่หน้าจอของคุณ โดยปกติคุณจะต้องพิมพ์รหัส Escape แบบสุ่มจำนวนหนึ่งโดยไม่ได้ตั้งใจ ซึ่งจะทำให้เทอร์มินัลของคุณยุ่งเหยิง โดยจะต้องมีไบต์ 0x1b อยู่ใน มีที่ไหนสักแห่งถ้าคุณ cat ไบนารีเพียงพอไปยังเทอร์มินัลของคุณ

คุณสามารถพิมพ์ Escape Sequence ด้วยตนเองได้หรือไม่?

ย้อนกลับไปบางส่วน เราได้พูดถึงวิธีที่คีย์ Home จับคู่กับ \x1b[H 3 ไบต์เหล่านั้นคือ Escape + [ + H (เพราะ Escape คือ \x1b )

และถ้าฉันพิมพ์ Escape ด้วยตนเอง จากนั้น [ จากนั้น H ในเทอร์มินัล xterm.js ฉันจะลงเอยที่จุดเริ่มต้นของบรรทัด เหมือนกับว่าฉันกด Home

ฉันสังเกตเห็นว่าสิ่งนี้ใช้ไม่ได้กับ fish บนคอมพิวเตอร์ของฉัน – ถ้าฉันพิมพ์ Escape แล้ว [ มันก็จะพิมพ์ออกมา [ แทนที่จะปล่อยให้ฉันดำเนินการลำดับการหลบหนีต่อไป ฉันถามเพื่อนของฉัน Jesse ที่เขียน โค้ด Terminal ของ Rust เกี่ยวกับเรื่องนี้ และ Jesse บอกฉันว่าโปรแกรมจำนวนมากใช้การ หมดเวลา สำหรับรหัส Escape – ถ้าคุณไม่กดปุ่มอื่นหลังจากเวลาขั้นต่ำผ่านไป ตัดสินใจว่าจริง ๆ แล้วไม่ใช่ Escape Code อีกต่อไป

เห็นได้ชัดว่าสิ่งนี้สามารถกำหนดค่าได้ในปลาด้วย fish_escape_delay_ms ดังนั้นฉันจึงรัน set fish_escape_delay_ms 1000 จากนั้นฉันก็สามารถพิมพ์รหัสการหลบหนีด้วยมือได้ เย็น!

การเข้ารหัสเทอร์มินัลค่อนข้างแปลก

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

  • Ctrl + a ทำสิ่งเดียวกันกับ Ctrl + Shift + a
  • Alt เหมือนกับ Escape
  • ลำดับการควบคุม (เช่น สี / การเลื่อนเคอร์เซอร์ไปรอบๆ) ใช้ไบต์เดียวกับปุ่ม Escape ดังนั้นคุณต้องอาศัยจังหวะเวลาในการพิจารณาว่ามันเป็นลำดับการควบคุมของผู้ใช้เพียงแค่กด Escape หรือไม่

แต่ทั้งหมดนี้ได้รับการออกแบบในยุค 70 หรือ 80 หรือบางอย่าง และจำเป็นต้องคงสภาพเดิมตลอดไปเพื่อความเข้ากันได้แบบย้อนหลัง นั่นคือสิ่งที่เราได้รับ 🙂

เปลี่ยนขนาดหน้าต่าง

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

นี่คือสิ่งที่ Go code ใน goterm ต้องทำ:

 syscall.Syscall( syscall.SYS_IOCTL, tty.Fd(), syscall.TIOCSWINSZ, uintptr(unsafe.Pointer(&resizeMessage)), )

นี่คือการใช้การเรียกระบบ ioctl ความเข้าใจของฉันเกี่ยวกับ ioctl คือการเรียกใช้ระบบสำหรับสิ่งสุ่มจำนวนมากที่ไม่ครอบคลุมโดยการเรียกระบบอื่น ๆ ซึ่งโดยทั่วไปเกี่ยวข้องกับ IO ฉันเดา

syscall.TIOCSWINSZ เป็นค่าคงที่จำนวนเต็มซึ่งบอก ioctl ว่าเราต้องการสิ่งใดในกรณีนี้ (เปลี่ยนขนาดหน้าต่างของเทอร์มินัล)

นี่เป็นวิธีการทำงานของ xterm เช่นกัน

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

นั่นคือทั้งหมดที่สำหรับตอนนี้!

มีอะไรให้รู้อีกมากเกี่ยวกับเทอร์มินัล (เราสามารถพูดคุยเกี่ยวกับสีหรือโหมดดิบเทียบกับปรุงสุกหรือการสนับสนุน Unicode หรืออินเทอร์เฟซ pseudoterminal ของ Linux) แต่ฉันจะหยุดที่นี่เพราะเวลา 22.00 น. มันค่อนข้างจะยาว และฉันคิดว่าสมองของฉันไม่สามารถจัดการกับข้อมูลใหม่ ๆ เกี่ยวกับเทอร์มินัลได้ในวันนี้

ขอบคุณ Jesse Luehrs ที่ตอบคำถามนับพันล้านข้อเกี่ยวกับเทอร์มินัล ข้อผิดพลาดทั้งหมดเป็นของฉัน 🙂

ตรวจสอบบริการเว็บขนาดเล็ก

สวัสดี! ฉันเพิ่งเริ่มใช้เซิร์ฟเวอร์เพิ่มอีกสองสามเครื่องเมื่อเร็ว ๆ นี้ ( nginx playground , ยุ่งกับ dns , dns lookup ) ดังนั้นฉันจึงคิดเกี่ยวกับการตรวจสอบ

ตอนแรกฉันตรวจสอบเว็บไซต์เหล่านี้ไม่ชัดเจนนัก ดังนั้นฉันจึงต้องการเขียนอย่างรวดเร็วว่าฉันทำอย่างไร

ฉันจะไม่พูดถึงวิธีการตรวจสอบเว็บไซต์ Big Serious Mission Critical เลย เฉพาะเว็บไซต์เล็กๆ ที่ไม่สำคัญเท่านั้น

เป้าหมาย: ใช้เวลาดำเนินการประมาณ 0 ครั้ง

ฉันต้องการให้ไซต์ทำงานเป็นส่วนใหญ่ แต่ฉันก็ต้องการใช้เวลาประมาณ 0% ของเวลาของฉันในการดำเนินงานที่กำลังดำเนินอยู่

ตอนแรกฉันระมัดระวังการใช้เซิร์ฟเวอร์มากเพราะในงานสุดท้ายของฉัน ฉันอยู่ในการหมุนเวียน 24 ถึง 7 วัน สำหรับบริการที่สำคัญบางอย่าง และในใจของฉัน “ความรับผิดชอบต่อเซิร์ฟเวอร์” หมายถึง “ตื่นนอนตอนตีสองเพื่อซ่อมเซิร์ฟเวอร์ ” และ “มีแดชบอร์ดที่ซับซ้อนมากมาย”

ดังนั้นในขณะที่ฉันสร้างเว็บไซต์แบบคงที่เพื่อที่ฉันจะได้ไม่ต้องคิดถึงเซิร์ฟเวอร์

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

ไม่มีการติดตามห่วยๆ

ตอนแรกฉันไม่ได้ตั้งค่าการตรวจสอบใด ๆ สำหรับเซิร์ฟเวอร์ของฉันเลย สิ่งนี้มีผลลัพธ์ที่คาดเดาได้อย่างมาก – บางครั้งเว็บไซต์ก็พัง และฉันไม่รู้เรื่องนี้จนกว่าจะมีคนบอกฉัน

ขั้นตอนที่ 1: ตัวตรวจสอบสถานะการออนไลน์

ขั้นตอนแรกคือการตั้งค่าตัวตรวจสอบเวลาทำงาน มีสิ่งเหล่านี้มากมายที่ฉันใช้อยู่ตอนนี้คือ updown.io และ uptime robot ฉันชอบส่วนต่อประสานผู้ใช้ของ updown มากกว่า แต่หุ่นยนต์ uptime มีระดับฟรีที่ใจกว้างมากกว่า

เหล่านี้

  1. ตรวจสอบว่าไซต์ขึ้น
  2. ถ้ามันลงไปมันจะส่งอีเมลถึงฉัน

ฉันพบว่าการแจ้งเตือนทางอีเมลเป็นระดับที่ดีสำหรับฉัน ฉันจะรู้ได้อย่างรวดเร็วว่าไซต์หยุดทำงานหรือไม่ แต่มันไม่ปลุกฉันหรืออะไรก็ตาม

ขั้นตอนที่ 2: การตรวจสุขภาพแบบ end-to-end

ต่อไป มาพูดถึงความหมายของคำว่า “ตรวจสอบว่าไซต์พร้อมใช้งาน” จริงๆ แล้ว

ตอนแรกฉันเพิ่งสร้างหนึ่งในจุดสิ้นสุดการตรวจสอบสุขภาพของฉันเป็นฟังก์ชันที่ส่งคืน 200 OK ไม่ว่าจะเกิดอะไรขึ้น

สิ่งนี้มีประโยชน์ – มันบอกฉันว่าเซิร์ฟเวอร์เปิดอยู่!

แต่ไม่น่าแปลกใจเลยที่ฉันพบปัญหาเพราะไม่ได้ตรวจสอบว่า API นั้นใช้ งาน ได้จริง – บางครั้งการตรวจสุขภาพก็สำเร็จแม้ว่าบริการที่เหลือจะเข้าสู่สถานะไม่ดีจริงๆ

ดังนั้นฉันจึงอัปเดตเพื่อสร้างคำขอ API จริงและทำให้แน่ใจว่าสำเร็จ

บริการทั้งหมดของฉันทำสิ่งเล็กๆ น้อยๆ (สนามเด็กเล่น nginx มีเพียง 1 ปลายทาง) ดังนั้นจึงค่อนข้างง่ายในการตั้งค่าการตรวจสุขภาพที่ดำเนินการจริงส่วนใหญ่ที่บริการควรทำ

นี่คือสิ่งที่ตัวจัดการการตรวจสุขภาพแบบ end-to-end สำหรับสนามเด็กเล่น nginx ดูเหมือน เป็นพื้นฐานมาก: มันแค่ส่งคำขอ POST อีกครั้ง (สำหรับตัวมันเอง) และตรวจสอบว่าคำขอนั้นสำเร็จหรือล้มเหลว

 func healthHandler(w http.ResponseWriter, r *http.Request) { // make a request to localhost:8080 with `healthcheckJSON` as the body // if it works, return 200 // if it doesn't, return 500 client := http.Client{} resp, err := client.Post("http://localhost:8080/", "application/json", strings.NewReader(healthcheckJSON)) if err != nil { log.Println(err) w.WriteHeader(http.StatusInternalServerError) return } if resp.StatusCode != http.StatusOK { log.Println(resp.StatusCode) w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) }

ความถี่ในการตรวจสุขภาพ: รายชั่วโมง

ตอนนี้ฉันกำลังตรวจสุขภาพเป็นส่วนใหญ่ทุกชั่วโมง และบางรายการทุก 30 นาที

ฉันเรียกใช้ทุกชั่วโมงเพราะราคาของ updown.io ต่อการตรวจสุขภาพ ฉันกำลังตรวจสอบ URL ที่แตกต่างกัน 18 รายการ และฉันต้องการรักษางบประมาณการตรวจสุขภาพของฉันให้น้อยที่สุดที่ $5/ปี

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

ถ้ามันเป็นอิสระที่จะเรียกใช้พวกเขาบ่อยขึ้น ฉันอาจจะเรียกใช้พวกเขาทุก 5-10 นาทีแทน

ขั้นตอนที่ 3: รีสตาร์ทโดยอัตโนมัติหากการตรวจสุขภาพล้มเหลว

บางเว็บไซต์ของฉันใช้ fly.io และ fly มีคุณสมบัติมาตรฐานที่ค่อนข้างดี ซึ่งฉันสามารถกำหนดค่า HTTP healthcheck สำหรับบริการและเริ่มต้นบริการใหม่ได้หากการตรวจสุขภาพเริ่มล้มเหลว

“รีสตาร์ทบ่อยมาก” เป็นกลยุทธ์ที่มีประโยชน์มากในการเขียนทับจุดบกพร่องที่ฉันยังไม่ได้แก้ไข – ในขณะที่สนามเด็กเล่น nginx มีการรั่วไหลของกระบวนการซึ่งกระบวนการ nginx ไม่ได้ถูกยกเลิก ดังนั้นเซิร์ฟเวอร์จึงทำงานจนหมด ของแรม

ด้วยการตรวจสุขภาพ ผลลัพธ์ของสิ่งนี้ก็คือ ทุกๆ วัน สิ่งนี้จะเกิดขึ้น:

  • เซิร์ฟเวอร์หมด RAM
  • การตรวจสุขภาพเริ่มล้มเหลว
  • มันเริ่มต้นใหม่
  • ทุกอย่างเรียบร้อยดีอีกแล้ว
  • ทำซ้ำเรื่องราวทั้งหมดอีกครั้งในอีกไม่กี่ชั่วโมงต่อมา

ในที่สุดฉันก็ได้แก้ไขการรั่วไหลของกระบวนการจริง ๆ แต่ก็ดีที่มีวิธีแก้ปัญหาที่สามารถทำให้สิ่งต่าง ๆ ทำงานต่อไปได้ในขณะที่ฉันผัดวันประกันพรุ่งแก้ไขข้อผิดพลาด

การตรวจสอบสภาพเหล่านี้เพื่อตัดสินใจว่าจะเริ่มต้นบริการใหม่ให้ทำงานบ่อยขึ้นหรือไม่: ทุกๆ 5 นาทีหรือมากกว่านั้น

นี่ไม่ใช่วิธีที่ดีที่สุดในการตรวจสอบ Big Services

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

มันทำงานได้ดีจนถึงตอนนี้!

ตอนแรกฉันเขียนโพสต์นี้เมื่อ 3 เดือนที่แล้วในเดือนเมษายน แต่ฉันรอจนถึงตอนนี้เพื่อเผยแพร่เพื่อให้แน่ใจว่าการตั้งค่าทั้งหมดใช้งานได้

มันสร้างความแตกต่างได้ค่อนข้างมาก – ก่อนที่ฉันจะมีปัญหาเรื่องการหยุดทำงานที่งี่เง่า และตอนนี้ในช่วงสองสามเดือนที่ผ่านมา ไซต์เพิ่มขึ้น 99.95% ของเวลาทั้งหมด!