TLS and DoS
บทเรียนที่ผ่านมาป้องกัน message รายตัวบนการเชื่อมต่อ บทเรียนสุดท้ายนี้ซูมออกไปยังชั้นการเชื่อมต่อเอง: bytes บนสาย และอันตรายของการปล่อยให้การเชื่อมต่อมากเกินไป — หรือการเชื่อมต่อที่ทำไม่เสร็จมากเกินไป — สะสมกองพะเนิน มีสองธีมที่วิ่งผ่านบทเรียนนี้: เข้ารหัสทุกอย่าง ด้วย TLS, และ อย่าใช้ทรัพยากรก่อนที่คุณจะรู้ว่าการเชื่อมต่อนั้นคุ้มค่า
ใช้ wss เสมอ — TLS ต่อรองไม่ได้
หัวข้อที่มีชื่อว่า “ใช้ wss เสมอ — TLS ต่อรองไม่ได้”WebSocket สืบทอดความปลอดภัยจาก scheme ที่เปิดบน ws:// คือ plaintext: handshake, auth token ของคุณ, และทุก frame เดินทางเป็น cleartext ที่ router, proxy, หรือ peer บน Wi-Fi ร้านกาแฟใด ๆ ระหว่าง client และ server อ่านหรือเขียนทับได้ wss:// ห่อทั้งหมดไว้ใน TLS เหมือนกับที่ https:// ทำให้ HTTP เป๊ะ ๆ กฎนั้นเรียบง่ายและเด็ดขาด:
- ใช้
wss://ใน production เสมอ Terminate TLS ที่ load balancer ของคุณหรือใน server โดยตรง - หน้าเว็บที่เสิร์ฟผ่าน
https://เปิดการเชื่อมต่อws://ไม่ได้ — เบราว์เซอร์บล็อกเป็น mixed content ดังนั้นเมื่อไซต์ของคุณอยู่บน HTTPS (ซึ่งก็อยู่อยู่แล้ว)wssก็เป็นตัวเลือกเดียวที่ใช้งานได้อยู่แล้ว - จงมอง
ws://เป็นความสะดวกในการพัฒนาบน localhost เท่านั้น ไม่มีอะไรมากกว่านั้น
// Production: TLS-terminated WebSocket server.import { createServer } from 'node:https';import { readFileSync } from 'node:fs';import { WebSocketServer } from 'ws';
const server = createServer({ cert: readFileSync('/etc/tls/fullchain.pem'), key: readFileSync('/etc/tls/privkey.pem'),});// maxPayload caps frame size at the protocol level (see the previous lesson).const wss = new WebSocketServer({ server, maxPayload: 64 * 1024 });server.listen(443);Denial of service ระดับการเชื่อมต่อ
หัวข้อที่มีชื่อว่า “Denial of service ระดับการเชื่อมต่อ”Rate limiting จำกัด message ภายใน การเชื่อมต่อ แต่ผู้โจมตีก็โจมตีคุณด้วยตัวการเชื่อมต่อ เอง ได้เช่นกัน:
- Connection flood เปิด socket หลายพันจาก IP เดียวหรือไม่กี่ IP socket ที่เปิดแต่ละอันกิน memory และ file descriptor; ถ้ามากพอก็ทำให้ server หมดก่อนที่จะส่ง message สักอัน
- Slowloris / slow handshake เริ่มการอัปเกรด HTTP แต่ส่ง bytes ของคำขอทีละนิดอย่างเชื่องช้า หรือไม่ทำให้จบเลย server ที่ถือการเชื่อมต่อเปิดค้างรออยู่จะกินช่องหนึ่งต่อผู้โจมตีหนึ่งคนฟรี ๆ การป้องกันคือ handshake timeout: ถ้าการอัปเกรดไม่เสร็จภายในไม่กี่วินาที ก็ตัดการเชื่อมต่อทิ้ง
- Idle hoarding เปิด socket, authenticate, แล้วเงียบไปตลอดกาล — ผูกทรัพยากรไว้โดยไม่ทำอะไรเลย heartbeat บวก idle timeout ทวงคืนพวกนี้ได้: ping เป็นระยะ และปิดการเชื่อมต่อใด ๆ ที่ pong ไม่กลับหรือไม่ส่งอะไรมานานเกินไป
flowchart TB
N["New TCP / upgrade attempt"] --> IP{"Connections from<br/>this IP < limit?"}
IP -- "no" --> R1["Refuse — protect capacity"]
IP -- "yes" --> HT{"Handshake completes<br/>within timeout?"}
HT -- "no (slowloris)" --> R2["Drop — reclaim slot"]
HT -- "yes" --> AU{"Authenticated?"}
AU -- "no" --> R3["Close 1008 — before allocating state"]
AU -- "yes" --> AL["Allocate per-connection state<br/>+ start heartbeat / idle timeout"] Authenticate ก่อนที่คุณจะจัดสรร
หัวข้อที่มีชื่อว่า “Authenticate ก่อนที่คุณจะจัดสรร”หลักการที่สำคัญที่สุดเพียงข้อเดียวตรงนี้คือ ลำดับ ปฏิเสธที่ราคาถูกก่อน และใช้ทรัพยากรเป็นอย่างสุดท้าย การเชื่อมต่อที่ยังไม่ authenticate หรือ origin ผิดควรถูกปิด ก่อน ที่คุณจะจัดสรร session object ของตัวเอง, ลงทะเบียนเข้าห้องของคุณ, subscribe เข้ากับ pub/sub backend ของคุณ, หรือ buffer อะไรไว้ให้ ถ้าคุณจัดสรรก่อนแล้วค่อยตรวจทีหลัง ผู้โจมตีที่ถูกปฏิเสธทุกคนก็ยังทำให้คุณเสีย setup เต็มรูปแบบไปแล้ว — ที่เป็น resource exhaustion ที่คุณกำลังพยายามป้องกันพอดี ท่อนี้เป็นแบบนี้เสมอ: IP/connection limit → handshake timeout → origin check → authenticate → ตอนนี้ค่อยจัดสรร state
Per-IP limit และ idle timeout ในทางปฏิบัติ
หัวข้อที่มีชื่อว่า “Per-IP limit และ idle timeout ในทางปฏิบัติ”ภาพร่างด้านล่างแสดงรูปร่างของ connection-handler: counter ราย IP ที่ปฏิเสธการเชื่อมต่อส่วนเกิน, การ cleanup counter นั้นทันทีตอน close, และ idle timeout ที่ขับด้วย heartbeat ซึ่งปิด socket ที่ตายแล้ว นี่เป็น ts มากกว่าจะเป็นเดโมที่รันได้เพราะขึ้นอยู่กับเครือข่ายจริงและนาฬิกาจริง — แต่หย่อนลงใน server ws ได้ตรง ๆ
const MAX_PER_IP = 20;const perIp = new Map<string, number>();
wss.on('connection', (ws, req) => { const ip = req.socket.remoteAddress ?? 'unknown';
// 1. Per-IP connection limit — refuse BEFORE allocating anything. const count = perIp.get(ip) ?? 0; if (count >= MAX_PER_IP) { ws.close(1008, 'too many connections'); return; } perIp.set(ip, count + 1);
// 2. (origin + auth checks happen here — see earlier lessons)
// 3. Heartbeat + idle timeout: reclaim dead/silent sockets. let alive = true; ws.on('pong', () => { alive = true; }); const beat = setInterval(() => { if (!alive) { ws.terminate(); return; } // no pong since last beat → kill alive = false; ws.ping(); }, 30_000);
// 4. Always clean up on close — release the IP slot and the timer. ws.on('close', () => { clearInterval(beat); perIp.set(ip, (perIp.get(ip) ?? 1) - 1); });});handshake timeout เองอยู่สูงขึ้นไปหนึ่งระดับ บน HTTP server (server.headersTimeout / server.requestTimeout) ดังนั้น slowloris upgrade จะถูกทิ้งก่อนที่จะไปถึง handler นี้ด้วยซ้ำ เมื่อรวมกันสิ่งเหล่านี้จำกัด จำนวนเท่าไร ของการเชื่อมต่อที่มีอยู่, นานแค่ไหน ที่อันที่ยังไม่เสร็จจะค้างอยู่ได้, และ นานแค่ไหน ที่อัน idle จะรอดอยู่ได้ — ปิดช่องว่างสุดท้ายที่ผู้โจมตีจะใช้ประโยชน์ได้
| Security Layer | ป้องกันอะไร | วิธีทำ |
|---|---|---|
| TLS (wss://) | Man-in-the-middle, eavesdropping | ใช้ wss:// เสมอใน production |
| Connection limit | DDoS จาก connection flood | max connection per IP, global limit |
| Message rate limit | Message flood | token bucket per connection |
| Payload validation | Malformed data attack | validate schema ทุก message |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”ใช้ ws:// ใน Production
อาการ:
- message ส่งผ่าน network โดยไม่ encrypt
- attacker ใน network เดียวกัน (café wifi) ดัก message ได้
- ใช้
wss://(WebSocket Secure) เสมอใน production
ไม่มี DDoS Protection บน WebSocket Endpoint อาการ:
- attacker เปิด WebSocket connection จำนวนมากจาก IP หลายตัว
- server file descriptor หมด — legitimate user เชื่อมต่อไม่ได้
- ใช้ Cloudflare, AWS Shield, หรือ nginx rate limit บน
/wsendpoint
💡 ตัวอย่างจากของจริง
Cloudflare WebSocket:
- TLS terminate ที่ edge — wss:// ถึง Cloudflare, แล้วส่งต่อผ่าน internal network
- DDoS protection built-in — absorb connection flood ก่อนถึง origin server
Discord:
- wss:// เสมอ — message ทุกชิ้น encrypt บน wire
- rate limit และ connection limit ป้องกัน abuse จาก bot farm