- IPv6 zone คือรูปแบบที่ใช้ระบุอินเทอร์เฟซปลายทาง เช่น
fe80::4%eth0 เมื่อหลายอินเทอร์เฟซใช้ช่วงลิงก์โลคัล fe80:: เดียวกัน
- ใน URL ที่อยู่ IPv6 ต้องครอบด้วยวงเล็บเหลี่ยมแบบ
[fe80::4]:80 เพื่อแยกพอร์ตออกจากโคลอนในที่อยู่ และเมื่อมี zone ด้วย รูปแบบ วงเล็บเหลี่ยม จะเป็น [fe80::4%eth0]:80
net/url ของ Go ตีความ ]:80 ผิดเป็น URL escape %et ที่ไม่ถูกต้อง จึงเกิดข้อผิดพลาด invalid URL escape
- RFC 6874 นิยาม IPv6 literal ที่มี zone เป็น
IPv6address "%25" ZoneID ดังนั้นใน URL ต้องทำ percent-encode ให้ % เป็น %25
- หาก Anubis ต้องการชี้ไปยังที่อยู่ IPv6 แบบมี zone ก็ต้องใช้รูปแบบนี้ และยังคงเป็น edge case ที่พัวพันถึงความเข้ากันได้ของ origin และไลบรารี เช่นปัญหาที่พบในเบราว์เซอร์, nginx และ Requests
การชนกันระหว่าง IPv6 zone กับไวยากรณ์ของ URL
- ที่อยู่ link-local ของ IPv6 สามารถอยู่ในช่วง
fe80::whatever ได้ในแต่ละอินเทอร์เฟซ ดังนั้นเมื่อมีสองอินเทอร์เฟซเครือข่ายและต้องแยกปลายทาง fe80::4 จึงจำเป็นต้องใช้ IPv6 scope/zone)
- รูปแบบของค่า zone แตกต่างกันไปตามระบบปฏิบัติการ โดย Linux ใช้ชื่ออินเทอร์เฟซ ส่วน Windows ใช้ ID ของอินเทอร์เฟซ
- ในตัวอย่าง
eth0 คือชื่ออุปกรณ์อีเธอร์เน็ต และที่อยู่จะแสดงดังนี้
fe80::4%eth0
- ปกติโฮสต์กับพอร์ตจะคั่นกันด้วยโคลอน แต่ที่อยู่ IPv6 ก็ใช้โคลอนคั่นกลุ่มเลขฐานสิบหกด้วย ดังนั้น
fe80::4 ที่พอร์ต 80 จึงต้องครอบด้วยวงเล็บเหลี่ยมดังนี้
[fe80::4]:80
- เมื่อรวม zone เข้าไปด้วย จะได้รูปแบบดังนี้
[fe80::4%eth0]:80
- หากใส่ลงในชื่อโฮสต์ของ URL ตรง ๆ เป็น
http://[fe80::4%eth0]:80 จะล้มเหลวใน Go net/url เพราะตีความ %et เป็น URL escape ที่ไม่ถูกต้อง
panic: parse "http://[fe80::4%eth0]:80": invalid URL escape "%et"
วิธีแก้ตามมาตรฐานและปัญหาที่ยังเหลือ
- ค่าที่ไม่ตรงกับไวยากรณ์ของ URL ต้องใช้ percent-encoding เช่น
%20 ใน URL คือการเข้ารหัสช่องว่าง ASCII ซึ่งใช้ตรง ๆ ใน URL ไม่ได้
- เนื่องจาก
% ของ IPv6 zone เองก็ต้องถูกเข้ารหัสเช่นกัน ใน Go จึงต้องเขียนเป็น http://[fe80::4%25eth0]:80 จึงจะได้ผลลัพธ์จาก Hostname() เป็น fe80::4%eth0
- RFC 9844 ให้แนวทางสำหรับการจัดการ IPv6 zone ในส่วนติดต่อผู้ใช้ และ RFC 6874 นิยามไวยากรณ์ของ IPv6 literal ที่มี zone ใน URL ไว้ดังนี้
IP-literal = "[" ( IPv6address / IPv6addrz / IPvFuture ) "]"
ZoneID = 1*( unreserved / pct-encoded )
IPv6addrz = IPv6address "%25" ZoneID
- edge case เดียวกันนี้ยังปรากฏใน ตั๋วของ nginx, อิสชูของ Requests และ ร่าง BCP สำหรับ HTTP link-local URI
- ปัจจุบันเบราว์เซอร์ยังไม่รองรับ IPv6 zone เพราะมันทำให้แนวคิดเรื่อง “origin” ซึ่งถูกใช้กับพฤติกรรมละเอียดอ่อนหลายอย่างพังลง และร่างดังกล่าวก็เป็นความพยายามที่จะนิยาม zone origin ของ IPv6 เพื่อให้เบราว์เซอร์ใช้งานได้
- หาก Anubis ต้องการชี้ไปยังที่อยู่ IPv6 แบบมี zone ก็จำเป็นต้องทำ percent-encoding ให้
% และภายใต้นโยบายที่ไม่ fork standard library ของ Go ก็ต้องยอมรับ UX ที่ไม่ดีของ edge case นี้
1 ความคิดเห็น
ความเห็นจาก Lobste.rs
TL;DR: คอมพิวเตอร์คือความผิดพลาดมันให้ความรู้สึกแบบ เทน้ำทิ้งทั้งอ่างพาลูกทิ้งไปด้วย ไหมพูดตรงตัวก็คือเป็นตรรกะแบบตัวร้ายในอนิเมะอย่าง Trigun ที่บอกว่ามนุษย์สามารถก่ออาชญากรรมอันน่าสยดสยองได้ งั้นก็ต้องกำจัดมนุษย์ทั้งหมด
ถึงจะรู้ว่าเป็นการพูดติดตลก แต่ก็น่าสนใจดี และผมตั้งใจจะเอาไปเป็นหัวข้อของงานพูดครั้งถัดไปที่กำลังเตรียมอยู่
แต่ก็เห็นด้วยกับใจความหลักอยู่ดี รู้สึกว่าสถานการณ์แบบนี้มันค่อนข้างน่าขันเหมือนกัน
แต่ในอีกด้านหนึ่ง กรณีที่จัดการที่อยู่ IPv6 แบบ link-local ได้ไม่ถูกต้องก็ไม่ได้หายากนัก
เพราะตอนนี้
%จะมีความหมายพิเศษเฉพาะในส่วน host ของ URL เท่านั้น และเฉพาะเมื่อ host เป็นที่อยู่ IPv6 และอยู่ภายใน[...]ไวยากรณ์อาจยังทำให้ไม่กำกวมได้ก็จริง แต่ประเด็นไม่ได้อยู่ตรงนั้น ยิ่งมีกรณียกเว้นแบบนี้มากขึ้น parser ของ URL ก็ยิ่งมีโอกาสพลาดข้อยกเว้นเฉพาะทาง และความต่างระหว่าง parser แต่ละตัวก็ยิ่งเปิดช่องให้บั๊กสกปรก ๆ หรือปัญหาด้านความปลอดภัยแฝงตัวเข้ามาได้ง่าย
ส่วนตัวผมเอนเอียงไปทางรองรับ IPv6 zone ใน URL แต่เมื่อก่อนเคยมีข้อแนะนำว่าต้อง URL-encode
%ดังนั้นถ้าจะย้อนกลับตอนนี้ก็จะเกิดความกำกวมจริง ๆ น่าเสียดายเหมือนกันpct-encoded = "%" HEXDIG HEXDIGโดย
HEXDIGถูกนิยามเป็น[a-fA-F]จึง parse%etเป็นลำดับที่ไม่ถูกต้องสเปกยังบอกอีกว่าอักขระ
%ถูกใช้เป็นตัวบ่งชี้ octet ที่ถูก percent-encode ดังนั้นถ้าจะใช้เป็นข้อมูลภายใน URI ก็ต้อง percent-encode เป็น%25การ decode สตริงที่ถูก decode มาแล้วซ้ำอีกครั้งอาจทำให้ octet ข้อมูลที่เป็นเปอร์เซ็นต์ถูกเข้าใจผิดว่าเป็นจุดเริ่มต้นของ percent-encoding และถ้า encode สตริงที่ถูก encode มาแล้วซ้ำอีกครั้งก็จะเกิดปัญหากลับด้าน สเปกจึงบอกว่า implementation ไม่ควร encode หรือ decode สตริงเดียวกันมากกว่าหนึ่งครั้งเพราะงั้นมันน่าหงุดหงิดก็จริง แต่ในทางปฏิบัติผมมองว่า เรียกว่าเป็นบั๊กได้ยาก
%ตรง ๆ ในที่อยู่ที่มี zone ถึงถูกมองว่าน่ากลัวขนาดนั้น อักขระที่ถูก encode อนุญาตให้ใช้ในส่วน host ได้อยู่แล้ว และการใช้%ตรง ๆ ในที่อยู่ zone ก็ทำให้เกิด ความกำกวม%เองไม่ใช่อักขระ unreserved ดังนั้นการ percent-encode มันจึงถูกต้องแล้วที่
net/urlและnet/httpของ Go ขัดกับ URL RFC ก็ไม่ใช่เรื่องใหม่ โดยเฉพาะการมีอยู่ของnet/url.URL.Pathและการใช้งานมันในnet/httpที่ค่อนข้างน่าปวดหัว เพราะมันทำให้%2Fพังได้net/http.Redirectก็ใช้path.Cleanจนไปยุบ//ให้เหลือ/แบบผิด ๆมีเหตุผลมากมายที่ทำให้คนอยาก fork ส่วนที่จัดการ URL ใน Go standard library หรือเสนออะไรอย่าง
net/url/v2แต่เท่าที่เห็นจากบทความนี้ การจัดการ ที่อยู่ IPv6 zone ของ Go ดูสมเหตุสมผลและค่อนข้างถูกต้อง