Line data Source code
1 : /*
2 : * Copyright (C) 2016 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 42 : import cockpit from "cockpit";
7 42 : import * as ipaddr from "ipaddr.js";
8 :
9 42 : const _ = cockpit.gettext;
10 :
11 : /* NetworkManager specific data conversions and utility functions.
12 : */
13 :
14 35 : let byteorder;
15 :
16 35 : export function set_byteorder(bo) {
17 35 : byteorder = bo;
18 35 : }
19 :
20 35 : export function ip_prefix_to_text(num) {
21 35 : return num.toString();
22 35 : }
23 :
24 4 : export function ip_prefix_from_text(text) {
25 4 : if (/^[0-9]+$/.test(text.trim()))
26 4 : return parseInt(text, 10);
27 :
28 1 : throw cockpit.format(_("Invalid prefix $0"), text);
29 4 : }
30 :
31 2 : export function ip_metric_to_text(num) {
32 2 : return num.toString();
33 2 : }
34 :
35 2 : export function ip_metric_from_text(text) {
36 2 : if (text === "")
37 0 : return 0;
38 :
39 2 : if (/^[0-9]+$/.test(text.trim()))
40 2 : return parseInt(text, 10);
41 :
42 0 : throw cockpit.format(_("Invalid metric $0"), text);
43 2 : }
44 :
45 7 : export function ip_network_address(address, prefix_len) {
46 7 : const addrCIDR = address.toString() + "/" + prefix_len;
47 2 : const ip_obj = address.kind() === "ipv4" ? ipaddr.IPv4 : ipaddr.IPv6;
48 :
49 7 : try {
50 7 : return ip_obj.networkAddressFromCIDR(addrCIDR).toString();
51 0 : } catch (_e) {
52 0 : return null;
53 0 : }
54 7 : }
55 :
56 7 : export function ip_first_usable_address(address, prefix) {
57 7 : try {
58 7 : const addr_cidr = address.toString() + "/" + prefix;
59 :
60 7 : if (address.kind() === "ipv4") {
61 7 : const netAddr = ipaddr.IPv4.networkAddressFromCIDR(addr_cidr);
62 0 : netAddr.octets[3] += (prefix < 31) ? 1 : 0;
63 7 : return netAddr.toString();
64 2 : } else {
65 2 : const netAddr = ipaddr.IPv6.networkAddressFromCIDR(addr_cidr);
66 0 : netAddr.parts[7] += (prefix < 127) ? 1 : 0;
67 2 : return netAddr.toString();
68 2 : }
69 0 : } catch (_e) {
70 0 : return null;
71 0 : }
72 7 : }
73 :
74 0 : function toDec(n) {
75 0 : return n.toString(10);
76 0 : }
77 :
78 0 : function bytes_from_nm32(num) {
79 0 : const bytes = [];
80 0 : if (byteorder == "be") {
81 0 : for (let i = 3; i >= 0; i--) {
82 0 : bytes[i] = num & 0xFF;
83 0 : num = num >>> 8;
84 0 : }
85 0 : } else if (byteorder == "le") {
86 0 : for (let i = 0; i < 4; i++) {
87 0 : bytes[i] = num & 0xFF;
88 0 : num = num >>> 8;
89 0 : }
90 0 : } else {
91 0 : throw new Error("byteorder is unset or has invalid value " + JSON.stringify(byteorder));
92 0 : }
93 0 : return bytes;
94 0 : }
95 :
96 10 : export function validate_ipv4(address) {
97 : // explicitly require all 4 octets
98 : // NM does not support any IPv4 short format
99 10 : return ipaddr.IPv4.isValidFourPartDecimal(address);
100 10 : }
101 :
102 8 : export function validate_ipv6(address) {
103 8 : return ipaddr.IPv6.isValid(address);
104 8 : }
105 :
106 10 : export function validate_ip(address) {
107 8 : return validate_ipv4(address) || validate_ipv6(address);
108 10 : }
109 :
110 0 : export function ip4_to_text(num, zero_is_empty) {
111 0 : if (num === 0 && zero_is_empty)
112 0 : return "";
113 0 : return bytes_from_nm32(num).map(toDec)
114 0 : .join('.');
115 0 : }
116 :
117 0 : export function ip4_from_text(text, empty_is_zero) {
118 0 : function invalid() {
119 0 : throw cockpit.format(_("Invalid address $0"), text);
120 0 : }
121 :
122 0 : if (text === "" && empty_is_zero)
123 0 : return 0;
124 :
125 0 : const parts = text.split('.');
126 0 : if (parts.length != 4)
127 0 : invalid();
128 :
129 0 : const bytes = parts.map(function(s) {
130 0 : if (/^[0-9]+$/.test(s.trim()))
131 0 : return parseInt(s, 10);
132 : else
133 0 : return invalid();
134 0 : });
135 :
136 0 : let num = 0;
137 0 : function shift(b) {
138 0 : if (isNaN(b) || b < 0 || b > 0xFF)
139 0 : invalid();
140 0 : num = 0x100 * num + b;
141 0 : }
142 :
143 0 : if (byteorder == "be") {
144 0 : for (let i = 0; i < 4; i++) {
145 0 : shift(bytes[i]);
146 0 : }
147 0 : } else if (byteorder == "le") {
148 0 : for (let i = 3; i >= 0; i--) {
149 0 : shift(bytes[i]);
150 0 : }
151 0 : } else {
152 0 : throw new Error("byteorder is unset or has invalid value " + JSON.stringify(byteorder));
153 0 : }
154 :
155 0 : return num;
156 0 : }
157 :
158 10 : export function ip4_prefix_from_text(prefix_mask) {
159 10 : const trimmed_mask = prefix_mask.trim();
160 10 : if (/^[0-9]+$/.test(trimmed_mask)) {
161 10 : return parseInt(prefix_mask, 10);
162 10 : }
163 :
164 : // make sure that mask has 4 octets format
165 3 : if (validate_ipv4(trimmed_mask)) {
166 3 : const prefix = ipaddr.IPv4.parse(trimmed_mask).prefixLengthFromSubnetMask();
167 3 : if (prefix !== null) {
168 3 : return prefix;
169 3 : }
170 3 : }
171 :
172 3 : throw cockpit.format(_("Invalid prefix or netmask $0"), prefix_mask);
173 10 : }
174 :
175 : // Shorten IPv6 address according to RFC 5952
176 : // https://datatracker.ietf.org/doc/html/rfc5952#section-4
177 : //
178 : // NetworkManager already handles dropping of leadin zeros within a single 16 bit field
179 : // but does not replace the longest consecutive zeros fields with "::".
180 0 : function ip6_shorten(ip6_addr) {
181 0 : function find_longest_zero(match) {
182 0 : let idx = -1;
183 0 : let length = 0;
184 :
185 0 : match.forEach((item, i) => {
186 0 : const count_zero = item[0].replaceAll(':', '').length;
187 0 : if (count_zero > length) {
188 0 : idx = i;
189 0 : length = count_zero;
190 0 : }
191 0 : });
192 :
193 0 : return idx;
194 0 : }
195 :
196 0 : const REGEX_MATCH_CONSECUTIVE_ZEROS = /\b:?(?:0:?){2,}/g;
197 0 : const match = [...ip6_addr.matchAll(REGEX_MATCH_CONSECUTIVE_ZEROS)];
198 :
199 : // nothing to shorten
200 0 : if (match.length === 0) {
201 0 : return ip6_addr;
202 0 : }
203 :
204 0 : const longest_idx = find_longest_zero(match);
205 : // replace first (leftmost) match
206 0 : const short_addr = ip6_addr.replace(match[longest_idx], "::");
207 :
208 0 : return short_addr;
209 0 : }
210 :
211 0 : export function ip6_to_text(data, zero_is_empty) {
212 0 : const parts = [];
213 0 : const bytes = cockpit.base64_decode(data);
214 0 : for (let i = 0; i < 8; i++)
215 0 : parts[i] = ((bytes[2 * i] << 8) + bytes[2 * i + 1]).toString(16);
216 0 : const result = parts.join(':');
217 0 : if (result == "0:0:0:0:0:0:0:0" && zero_is_empty)
218 0 : return "";
219 0 : return ip6_shorten(result);
220 0 : }
221 :
222 0 : export function ip6_from_text(text, empty_is_zero) {
223 0 : function invalid() {
224 0 : throw cockpit.format(_("Invalid address $0"), text);
225 0 : }
226 :
227 0 : if (text === "" && empty_is_zero)
228 0 : return cockpit.base64_encode([0, 0, 0, 0, 0, 0, 0, 0,
229 0 : 0, 0, 0, 0, 0, 0, 0, 0,
230 0 : ]);
231 :
232 0 : const parts = text.split(':');
233 0 : if (parts.length < 1 || parts.length > 8)
234 0 : invalid();
235 :
236 0 : if (parts[0] === "")
237 0 : parts[0] = "0";
238 0 : if (parts[parts.length - 1] === "")
239 0 : parts[parts.length - 1] = "0";
240 :
241 0 : const bytes = [];
242 0 : let empty_seen = false;
243 0 : let j = 0;
244 0 : for (let i = 0; i < parts.length; i++, j++) {
245 0 : if (parts[i] === "") {
246 0 : if (empty_seen)
247 0 : invalid();
248 0 : empty_seen = true;
249 0 : while (j < i + (8 - parts.length)) {
250 0 : bytes[2 * j] = bytes[2 * j + 1] = 0;
251 0 : j++;
252 0 : }
253 0 : } else {
254 0 : if (!/^[0-9a-fA-F]+$/.test(parts[i].trim()))
255 0 : invalid();
256 0 : const n = parseInt(parts[i], 16);
257 0 : if (isNaN(n) || n < 0 || n > 0xFFFF)
258 0 : invalid();
259 0 : bytes[2 * j] = n >> 8;
260 0 : bytes[2 * j + 1] = n & 0xFF;
261 0 : }
262 0 : }
263 0 : if (j != 8)
264 0 : invalid();
265 :
266 0 : return cockpit.base64_encode(bytes);
267 0 : }
268 :
269 : // SSID comes as a base64-encoded string from D-Bus (signature 'ay')
270 0 : export const ssid_from_nm = bytes => new TextDecoder().decode(
271 0 : new Uint8Array(cockpit.base64_decode(bytes ?? []))
272 0 : );
273 :
274 0 : export const ssid_to_nm = ssid => cockpit.base64_encode(new TextEncoder().encode(ssid));
275 :
276 42 : export function list_interfaces() {
277 42 : return new Promise((resolve, reject) => {
278 42 : const client = cockpit.dbus("org.freedesktop.NetworkManager");
279 42 : client.call('/org/freedesktop/NetworkManager',
280 42 : 'org.freedesktop.NetworkManager',
281 42 : 'GetAllDevices', [])
282 42 : .then(reply => {
283 42 : Promise.all(reply[0].map(device => {
284 42 : return Promise.all([
285 42 : client.call(device,
286 42 : 'org.freedesktop.DBus.Properties',
287 42 : 'Get', ['org.freedesktop.NetworkManager.Device', 'Interface'])
288 42 : .then(reply => reply[0]),
289 42 : client.call(device,
290 42 : 'org.freedesktop.DBus.Properties',
291 42 : 'Get', ['org.freedesktop.NetworkManager.Device', 'Capabilities'])
292 42 : .then(reply => reply[0])
293 42 : ]);
294 42 : }))
295 42 : .then(interfaces => {
296 42 : client.close();
297 42 : resolve(interfaces.map(i => {
298 42 : return { device: i[0].v, capabilities: i[1].v };
299 42 : }));
300 42 : })
301 0 : .catch(e => console.warn(JSON.stringify(e)));
302 42 : })
303 0 : .catch(e => console.warn(JSON.stringify(e)));
304 42 : });
305 42 : }
306 :
307 : // Check if we are working with non-persistent multi-connection,
308 : // namely the `id=Wired Connection` generated by networkmanager.
309 : //
310 : // When editing this connection, we need to create a new connection
311 : // on top of this, remove the multiconnection settings and assign it directly
312 : // to interface. This is done for Anaconda in order to avoid creating unexpected
313 : // multiconnections when user modifies the connection during installation.
314 21 : export function isNonPersistentMultiCon(connection) {
315 19 : const settings = connection?.Settings;
316 21 : if (settings &&
317 19 : settings.connection.interface_name === undefined &&
318 3 : settings.connection.id === "Wired Connection" &&
319 2 : connection.Unsaved === true && settings.connection.type &&
320 2 : settings.connection.multi_connect === 3 &&
321 2 : settings.connection.type === "802-3-ethernet") {
322 2 : return true;
323 2 : }
324 :
325 21 : return false;
326 21 : }
327 :
328 1 : export function createNewConnSettings(settings, iface_name) {
329 1 : const newSettings = {
330 1 : ...settings,
331 1 : connection: {
332 1 : ...settings.connection,
333 1 : id: iface_name,
334 1 : interface_name: iface_name,
335 : // increase the current priority by 1, maximum priority is 999
336 1 : autoconnect_priority: Math.min(settings.connection.autoconnect_priority + 1, 999),
337 1 : }
338 1 : };
339 1 : delete newSettings.connection.uuid;
340 1 : delete newSettings.connection.multi_connect;
341 :
342 1 : return newSettings;
343 1 : }
344 :
345 0 : export function debug() {
346 0 : if (window.debugging == "all" || window.debugging?.includes("networkmanager")) // not-covered: debugging
347 0 : console.debug("networkmanager:", ...arguments); // not-covered: debugging
348 0 : }
|