/* * Copyright (C) 2021 Red Hat, Inc. * SPDX-License-Identifier: LGPL-2.1-or-later */ import React, { useState, useContext, useEffect } from 'react'; import cockpit from 'cockpit'; import * as ipaddr from "ipaddr.js"; import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js"; import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js"; import { FormFieldGroup, FormFieldGroupHeader, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js"; import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect/index.js"; import { Grid } from "@patternfly/react-core/dist/esm/layouts/Grid/index.js"; import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js"; import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js"; import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js"; import { PlusIcon, TrashIcon } from '@patternfly/react-icons'; import { NetworkModal, dialogSave } from './dialogs-common.jsx'; import { ModelContext } from './model-context.jsx'; import { useDialogs } from "dialogs.jsx"; import { ip_first_usable_address, ip_network_address, validate_ip, ip_prefix_from_text, ip4_prefix_from_text } from './utils.js'; const _ = cockpit.gettext; const ip_method_choices = [ { choice: 'auto', title: _("Automatic") }, { choice: 'dhcp', title: _("Automatic (DHCP only)") }, { choice: 'link-local', title: _("Link local") }, { choice: 'manual', title: _("Manual") }, { choice: 'ignore', title: _("Ignore") }, { choice: 'shared', title: _("Shared") }, { choice: 'disabled', title: _("Disabled") } ]; const supported_ipv4_methods = ['auto', 'link-local', 'manual', 'shared', 'disabled']; // NM only supports a subset of IPv4 and IPv6 methods for wireguard // See: https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/1.42.8/src/libnm-core-impl/nm-setting-wireguard.c#L1723 const wg_supported_ipv4_methods = ['manual', 'disabled']; const wg_supported_ipv6_methods = ['link-local', 'manual', 'ignored', 'disabled']; export function get_ip_method_choices(topic, device_type) { if (topic === 'ipv4') { if (device_type === 'wireguard') return ip_method_choices.filter(item => wg_supported_ipv4_methods.includes(item.choice)); return ip_method_choices.filter(item => supported_ipv4_methods.includes(item.choice)); } if (device_type === 'wireguard') return ip_method_choices.filter(item => wg_supported_ipv6_methods.includes(item.choice)); // IPv6 supports all the choices return ip_method_choices; } export const IpSettingsDialog = ({ topic, connection, dev, settings }) => { const Dialogs = useDialogs(); const idPrefix = "network-ip-settings"; const model = useContext(ModelContext); const params = settings[topic]; const [addresses, setAddresses] = useState(params.address_data); const [defaultGateway, setDefaultGateway] = useState(params.gateway); const [gatewaySetExplicitly, setGatewaySetExplicitly] = useState(false); const [dialogError, setDialogError] = useState(undefined); const [dns, setDns] = useState(params.dns_data || []); const [dnsSearch, setDnsSearch] = useState(params.dns_search || []); const [ignoreAutoDns, setIgnoreAutoDns] = useState(params.ignore_auto_dns); const [ignoreAutoRoutes, setIgnoreAutoRoutes] = useState(params.ignore_auto_routes); const [method, setMethod] = useState(params.method); const [routes, setRoutes] = useState(params.route_data); // The link local, shared, and disabled methods can't take any // addresses, dns servers, or dns search domains. Routes, // however, are ok, even for "disabled" and "ignored". But // since that doesn't make sense, we remove routes as well for // these methods. const isOff = (method === "disabled" || method === "ignore"); const canHaveExtra = !(method === "link-local" || method === "shared" || isOff); // The auto_*_btns only make sense when the address method // is "auto" or "dhcp". const canAuto = (method === "auto" || method === "dhcp"); const prefixText = (topic === "ipv4") ? _("Prefix length or netmask") : _("Prefix length"); useEffect(() => { // The manual method needs at least one address if (method === 'manual' && addresses.length === 0) setAddresses([{ address: "", prefix: "" }]); if (!canHaveExtra) { setAddresses([]); setDns([]); setDnsSearch([]); } if (isOff) setRoutes([]); }, [method, addresses.length, canHaveExtra, isOff]); const onSubmit = (_ev) => { const createSettingsObj = () => ({ ...settings, [topic]: { ...settings[topic], method, address_data: addresses, gateway: defaultGateway, dns_data: dns, dns_search: dnsSearch, route_data: routes, ignore_auto_dns: ignoreAutoDns, ignore_auto_routes: ignoreAutoRoutes, } }); dialogSave({ model, dev, connection, settings: createSettingsObj(), setDialogError, onClose: Dialogs.close, }); }; const ipDefaultPrefix = (address) => { if (address.kind() === "ipv6") { return "64"; } // use classful IPv4 prefixes when only host address is specified const octets = address.octets; if (octets[0] >= 0 && octets[0] <= 127) { return "8"; } else if (octets[0] >= 128 && octets[0] <= 191) { return "16"; } else if (octets[0] >= 192 && octets[0] <= 223) { return "24"; } return ""; }; const addressHelper = (address_str, prefix_str, i, prefixField) => { const config = { address: address_str, prefix: prefix_str }; if (!validate_ip(address_str)) { return config; } const address = ipaddr.parse(address_str); if (address.kind() !== topic) { return config; } if (prefix_str === "" && !prefixField) { config.prefix = ipDefaultPrefix(address); } // prefix_str can contain prefix or IPv4 subnet mask let numericPrefix; try { numericPrefix = (address.kind() === "ipv4") ? ip4_prefix_from_text(config.prefix) : ip_prefix_from_text(config.prefix); } catch (_e) { return config; } // do not set gateway for last three prefixes // /30 and /126 only has two usable addresses // /31 and /127 is a point-to-point link with no gateway // /32 and /128 is a single host address const maxPrefix = (address.kind() === "ipv4") ? 30 : 126; if (i === 0 && numericPrefix < maxPrefix && !gatewaySetExplicitly) { const netAddr = ip_network_address(address, numericPrefix); const firstAddr = ip_first_usable_address(address, numericPrefix); const addrCompactStr = address.toString(); // do not set the default gateway automatically if the host address // is the first address in the subnet or network address if (firstAddr !== null && addrCompactStr !== firstAddr && netAddr !== null && netAddr !== addrCompactStr) { setDefaultGateway(firstAddr); } else { setDefaultGateway(""); } } else if (!gatewaySetExplicitly) { // reset setDefaultGateway(""); } return config; }; const removeAddress = (i) => { // also reset gateway when removing the last address if (addresses.length === 1) { setDefaultGateway(""); setGatewaySetExplicitly(false); } setAddresses(addresses.filter((_, index) => index !== i)); }; // Prefer device type if the device exists, otherwise fallback to a connection type // of an existing connection that is down in which case the device may not exist. const deviceType = dev?.DeviceType ?? connection?.Settings.connection.type; return ( setMethod(val)} value={method}> {get_ip_method_choices(topic, deviceType).map(choice => )}