v / vlib / net / ipv6.v
256 lines · 234 sloc · 6.63 KB · 5696dc8da95388d0c3906688619582df87628097
Raw
1module net
2
3// IPv6 text representation per RFC 5952 (August 2010).
4//
5// Pure-V replacement for libc's inet_ntop on the IPv6 path. inet_ntop
6// historically follows the older RFC 1884 / RFC 2373 rules and emits
7// the deprecated IPv4-compatible mixed form (`::a.b.c.d`) for any
8// address whose upper 96 bits are zero — non-conformant per RFC 5952
9// §5, which restricts the mixed form to recognized IPv4-in-IPv6
10// formats.
11//
12// Reference: https://www.rfc-editor.org/rfc/rfc5952
13//
14// Conformance summary:
15// §4.1 Leading zeros MUST be suppressed. ✓
16// §4.2.1 "::" MUST be used to its maximum capability. ✓
17// §4.2.2 "::" MUST NOT shorten just one 16-bit 0 field. ✓
18// §4.2.3 Longest run of zeros wins; ties: first run wins. ✓
19// §4.3 Hex digits a-f MUST be lowercase. ✓
20// §5 IPv4-mapped (::ffff:0:0/96) uses dotted-quad ✓
21// tail; the leading zeros and 0xffff are still
22// formatted per §4. Other IPv4-in-IPv6 formats
23// (ISATAP, IPv4-translatable) are not detectable
24// from the address bits alone and stay in hex.
25
26// canonical_ipv6_from_bytes formats a 16-byte IPv6 address as the
27// canonical text representation defined by RFC 5952. Errors only when
28// the input slice is not exactly 16 octets long.
29pub fn canonical_ipv6_from_bytes(b []u8) !string {
30 if b.len != 16 {
31 return error('canonical_ipv6_from_bytes: need 16 bytes, got ${b.len}')
32 }
33 mut groups := [8]u16{}
34 for i in 0 .. 8 {
35 groups[i] = (u16(b[2 * i]) << 8) | u16(b[2 * i + 1])
36 }
37 return format_ipv6_groups(groups)
38}
39
40// canonical_ipv6 takes any RFC 4291 legitimate text form (full,
41// "::"-compressed, or with an embedded IPv4 dotted-quad tail) and
42// returns the RFC 5952 canonical form.
43pub fn canonical_ipv6(addr string) !string {
44 bytes := parse_ipv6_to_bytes(addr)!
45 return canonical_ipv6_from_bytes(bytes)
46}
47
48// format_ipv6_groups turns 8 u16 groups into the canonical RFC 5952
49// string. Only the IPv4-mapped prefix (::ffff:0:0/96, RFC 4291)
50// triggers mixed notation here — other RFC 5952 §5 prefixes
51// (IPv4-translatable RFC 6052, ISATAP) require out-of-band knowledge
52// and are not assumed.
53fn format_ipv6_groups(g [8]u16) string {
54 if is_ipv4_mapped(g) {
55 a := g[6] >> 8
56 b := g[6] & 0xff
57 c := g[7] >> 8
58 d := g[7] & 0xff
59 return '::ffff:${a}.${b}.${c}.${d}'
60 }
61
62 start, length := longest_zero_run(g)
63
64 mut parts := []string{cap: 8}
65 for v in g {
66 parts << v.hex()
67 }
68
69 if length < 2 {
70 return parts.join(':')
71 }
72
73 mut out := ''
74 if start > 0 {
75 out += parts[..start].join(':')
76 }
77 out += '::'
78 end := start + length
79 if end < 8 {
80 out += parts[end..].join(':')
81 }
82 return out
83}
84
85// is_ipv4_mapped tests for the ::ffff:0:0/96 prefix (RFC 4291 §2.5.5.2).
86fn is_ipv4_mapped(g [8]u16) bool {
87 return g[0] == 0 && g[1] == 0 && g[2] == 0 && g[3] == 0 && g[4] == 0 && g[5] == 0xffff
88}
89
90// longest_zero_run scans the 8 groups for the longest contiguous run of
91// 0x0000 fields and returns (start_index, length). On ties the FIRST
92// run wins (RFC 5952 §4.2.3). When no zero is present, length is 0.
93fn longest_zero_run(g [8]u16) (int, int) {
94 mut best_start := 0
95 mut best_len := 0
96 mut cur_start := 0
97 mut cur_len := 0
98 for i in 0 .. 8 {
99 if g[i] == 0 {
100 if cur_len == 0 {
101 cur_start = i
102 }
103 cur_len++
104 if cur_len > best_len {
105 best_len = cur_len
106 best_start = cur_start
107 }
108 } else {
109 cur_len = 0
110 }
111 }
112 return best_start, best_len
113}
114
115// parse_ipv6_to_bytes accepts the legitimate RFC 4291 forms:
116// - 8 groups of 1..4 hex digits separated by ':'
117// - the same with one "::" run replacing one or more all-zero groups
118// - the same with the trailing 32 bits given as dotted-quad IPv4
119// Returns 16 octets in network order.
120fn parse_ipv6_to_bytes(s string) ![]u8 {
121 if s == '' {
122 return error('parse_ipv6: empty')
123 }
124 double_idx := s.index('::') or { -1 }
125 if s.count('::') > 1 {
126 return error('parse_ipv6: multiple "::" runs')
127 }
128
129 head, tail := if double_idx >= 0 {
130 s[..double_idx], s[double_idx + 2..]
131 } else {
132 s, ''
133 }
134
135 mut head_g := if head == '' { []string{} } else { head.split(':') }
136 mut tail_g := if tail == '' { []string{} } else { tail.split(':') }
137 mut v4_bytes := []u8{}
138
139 // Detect dotted-quad tail in the LAST group (of tail when "::" present,
140 // of head otherwise).
141 tail_owns_last := double_idx >= 0
142 last_group := if tail_owns_last && tail_g.len > 0 {
143 tail_g[tail_g.len - 1]
144 } else if !tail_owns_last && head_g.len > 0 {
145 head_g[head_g.len - 1]
146 } else {
147 ''
148 }
149 if last_group.contains('.') {
150 v4_bytes = parse_dotted_quad(last_group)!
151 if tail_owns_last {
152 tail_g = tail_g[..tail_g.len - 1].clone()
153 } else {
154 head_g = head_g[..head_g.len - 1].clone()
155 }
156 }
157
158 mut head_words := []u16{}
159 for h in head_g {
160 head_words << parse_hex_group(h)!
161 }
162 mut tail_words := []u16{}
163 for t in tail_g {
164 tail_words << parse_hex_group(t)!
165 }
166
167 v4_word_len := if v4_bytes.len == 4 { 2 } else { 0 }
168 total := head_words.len + tail_words.len + v4_word_len
169 if double_idx < 0 {
170 if total != 8 {
171 return error('parse_ipv6: ${s} must have exactly 8 groups (got ${total})')
172 }
173 } else {
174 if total >= 8 {
175 return error('parse_ipv6: "::" present but address already has ${total} groups')
176 }
177 }
178
179 mut words := []u16{cap: 8}
180 for w in head_words {
181 words << w
182 }
183 if double_idx >= 0 {
184 for _ in 0 .. (8 - total) {
185 words << u16(0)
186 }
187 }
188 for w in tail_words {
189 words << w
190 }
191 if v4_bytes.len == 4 {
192 words << (u16(v4_bytes[0]) << 8) | u16(v4_bytes[1])
193 words << (u16(v4_bytes[2]) << 8) | u16(v4_bytes[3])
194 }
195
196 if words.len != 8 {
197 return error('parse_ipv6: internal: expanded to ${words.len} groups')
198 }
199
200 mut out := []u8{cap: 16}
201 for w in words {
202 out << u8(w >> 8)
203 out << u8(w & 0xff)
204 }
205 return out
206}
207
208fn parse_hex_group(s string) !u16 {
209 if s == '' || s.len > 4 {
210 return error('parse_ipv6: bad hex group "${s}"')
211 }
212 mut v := u32(0)
213 for c in s {
214 d := hex_digit(c) or { return error('parse_ipv6: non-hex char in "${s}"') }
215 v = (v << 4) | u32(d)
216 }
217 return u16(v)
218}
219
220fn hex_digit(c u8) ?u8 {
221 if c >= `0` && c <= `9` {
222 return u8(c - `0`)
223 }
224 if c >= `a` && c <= `f` {
225 return u8(c - `a` + 10)
226 }
227 if c >= `A` && c <= `F` {
228 return u8(c - `A` + 10)
229 }
230 return none
231}
232
233fn parse_dotted_quad(s string) ![]u8 {
234 parts := s.split('.')
235 if parts.len != 4 {
236 return error('parse_ipv6: bad dotted-quad "${s}"')
237 }
238 mut out := []u8{cap: 4}
239 for p in parts {
240 if p.len == 0 || p.len > 3 {
241 return error('parse_ipv6: bad octet "${p}"')
242 }
243 mut v := u32(0)
244 for c in p {
245 if c < `0` || c > `9` {
246 return error('parse_ipv6: non-digit in octet "${p}"')
247 }
248 v = v * 10 + u32(c - `0`)
249 }
250 if v > 255 {
251 return error('parse_ipv6: octet "${p}" > 255')
252 }
253 out << u8(v)
254 }
255 return out
256}
257