This commit is contained in:
Tomáš Matuš 2026-11-04 09:04:41 +07:00 committed by GitHub
commit ba8b7d35ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 545 additions and 141 deletions

View file

@ -18,21 +18,21 @@ import {
is_managed,
is_wireless,
} from "../interfaces.js";
import { NetworkInterfacePage } from "../network-interface.jsx";
import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
import { AnacondaNetworkInterface } from "./anaconda-network-interface.jsx";
import { NMInterface } from "../utils.js";
const _ = cockpit.gettext;
interface AnacondaInterfaceRowProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
iface: any;
iface: NMInterface;
setSelectedIfaceName: React.Dispatch<React.SetStateAction<string | null>>;
}
const AnacondaInterfaceRow = ({ iface, setSelectedIfaceName }: AnacondaInterfaceRowProps) => {
const dev = iface.Device;
// Show WiFi name in the list
const ssid = dev.ActiveAccessPoint ? dev.ActiveAccessPoint.Ssid : null;
const ssid = dev?.ActiveAccessPoint ? dev?.ActiveAccessPoint.Ssid : null;
const ifaceDisplayName = ssid ? `${iface.Name} (${ssid})` : iface.Name;
const connectionStatus = dev.ActiveConnection ? _("Connected") : _("Disconnected");
@ -55,13 +55,11 @@ const AnacondaInterfaceRow = ({ iface, setSelectedIfaceName }: AnacondaInterface
};
interface AnacondaNetworkPageProps {
privileged: boolean;
operationInProgress: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interfaces: any[];
interfaces: NMInterface[];
}
export const AnacondaNetworkPage = ({ privileged, operationInProgress, interfaces }: AnacondaNetworkPageProps) => {
export const AnacondaNetworkPage = ({ operationInProgress, interfaces }: AnacondaNetworkPageProps) => {
const [selectedIfaceName, setSelectedIfaceName] = useState<string | null>(null);
const selectedIface = interfaces.find(iface => iface.Name === selectedIfaceName) ?? null;
@ -99,7 +97,7 @@ export const AnacondaNetworkPage = ({ privileged, operationInProgress, interface
return (
<Page data-test-wait={operationInProgress} id="networking" className="pf-m-no-sidebar anaconda">
<Split hasGutter id="networking-interfaces">
<Split hasGutter id="networking-interfaces" isWrappable>
<SplitItem>
<SimpleList>
{managedWireless.length !== 0 &&
@ -115,13 +113,7 @@ export const AnacondaNetworkPage = ({ privileged, operationInProgress, interface
</SplitItem>
<SplitItem isFilled>
{selectedIface
? <NetworkInterfacePage
privileged={privileged}
operationInProgress={operationInProgress}
usage_monitor={undefined}
plot_state={undefined}
interfaces={interfaces}
iface={selectedIface} />
? <AnacondaNetworkInterface iface={selectedIface} />
: <EmptyStatePanel
title={_("No interface selected")}
paragraph={_("Select a network interface to configure the connection.")}

View file

@ -0,0 +1,279 @@
/*
* Copyright (C) 2026 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import cockpit from "cockpit";
import React, { useContext, useState } from 'react';
import {
DescriptionList,
DescriptionListDescription,
DescriptionListGroup,
DescriptionListTerm,
} from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
import { ExpandableSection } from "@patternfly/react-core/dist/esm/components/ExpandableSection/index.js";
import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
import { ModelContext } from "../model-context";
import { useEvent } from "hooks";
import { active_connection_gateway, active_connection_ipv4, active_connection_ipv6, choice_title, connectInterface, createGhostConnectionSettings, disconnectInterface, is_managed, is_wireless, toggleConnectionAutoconnect, } from "../interfaces";
import { get_ip_method_choices } from "../ip-settings";
import { NetworkAction } from "../dialogs-common.jsx";
import { apSecurityFlagsToString, NMConnectionSettings, NMDevice, NMInterface } from "../utils";
import { Card, CardBody, CardHeader, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
const _ = cockpit.gettext;
interface IpConfig {
addresses: string[];
gateway: string;
dns_data: string[];
}
type IPTopic = "ipv4" | "ipv6";
interface IPConfigExpandableProps {
toggleText: string;
onToggle: any;
isExpanded: boolean;
iface: NMInterface;
settings: NMConnectionSettings;
topic: IPTopic;
}
// Get IP config, use what is available on ActiveConnection if the device is up.
// If it is down only show what we can get from the Connection settings.
const getIpConfig = (dev: NMDevice, settings: NMConnectionSettings, topic: IPTopic) => {
const ipSettings: IpConfig = { addresses: [], gateway: "", dns_data: [] };
const hasActiveCon = dev.ActiveConnection;
const params = settings[topic];
if (!params) {
return ipSettings;
}
// IP address
if (hasActiveCon) {
ipSettings.addresses = (topic === "ipv4")
? active_connection_ipv4(dev)
: active_connection_ipv6(dev, false);
} else {
ipSettings.addresses = params.address_data?.map((a) => a.address + "/" + a.prefix);
}
// show "choice title" if we get nothing
if (ipSettings.addresses.length === 0 && params.method != "manual") {
ipSettings.addresses = [choice_title(get_ip_method_choices(topic), params.method, _("Unknown configuration"))];
}
// Gateway
if (hasActiveCon) {
ipSettings.gateway = active_connection_gateway(dev, topic);
} else {
ipSettings.gateway = params.gateway || _("Not configured");
}
// DNS
ipSettings.dns_data = params.dns_data;
return ipSettings;
};
const IPConfigExpandable = ({
toggleText,
onToggle,
isExpanded,
iface,
settings,
topic,
}: IPConfigExpandableProps) => {
const ipSettings = getIpConfig(iface.Device, settings, topic);
const descriptionGroups = [
{
term: _("IP address"),
description: ipSettings.addresses.join(", "),
},
{
term: _("Default gateway"),
description: ipSettings.gateway,
},
{
term: _("DNS"),
description: ipSettings.dns_data.join(", "),
},
];
return (
<Flex className="ip-expandable"
alignItems={{ default: "alignItemsFlexStart" }}
flexWrap={{ default: "nowrap" }}
>
<FlexItem flex={{ default: "flex_1" }}>
<ExpandableSection
toggleText={toggleText}
onToggle={(_event, expanded) => onToggle(expanded)}
isExpanded={isExpanded}
isIndented
>
<DescriptionList aria-label={_("IP configuration")}>
{descriptionGroups.filter(group => !!group.description).map(group => (
<DescriptionListGroup key={group.term}>
<DescriptionListTerm>{group.term}</DescriptionListTerm>
<DescriptionListDescription>{group.description}</DescriptionListDescription>
</DescriptionListGroup>
))}
</DescriptionList>
</ExpandableSection>
</FlexItem>
<FlexItem>
<NetworkAction buttonText={_("Edit")}
iface={iface}
connectionSettings={settings}
type={topic}
buttonVariant="secondary"
isInline={false}
/>
</FlexItem>
</Flex>
);
};
interface DescriptionListWithAction {
dlTerm: React.ReactNode;
dlDescription: React.ReactNode;
actionButton?: React.ReactNode;
}
const DescriptionListWithAction = ({
dlTerm,
dlDescription,
actionButton,
}: DescriptionListWithAction) => {
return (
<Flex alignItems={{ default: "alignItemsFlexStart" }}
justifyContent={{ default: "justifyContentSpaceBetween" }}
className="description-list-with-action"
>
<FlexItem grow={{ default: "grow" }}>
<DescriptionList>
<DescriptionListGroup>
<DescriptionListTerm>{dlTerm}</DescriptionListTerm>
<DescriptionListDescription>{dlDescription}</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
</FlexItem>
{ actionButton &&
<FlexItem>
{actionButton}
</FlexItem>
}
</Flex>
);
};
const connectionState = (dev: NMDevice, isManaged: boolean) => {
if (!dev) {
return _("Inactive");
} else if (isManaged) {
return dev.StateText;
}
return dev.ActiveConnection ? _("Connected") : _("Disconnected");
};
interface AnacondaNetworkInterfaceProps {
iface: NMInterface;
}
export const AnacondaNetworkInterface = ({
iface,
}: AnacondaNetworkInterfaceProps) => {
const model = useContext(ModelContext);
useEvent(model, "changed");
console.log(iface);
const [isIpv4Expanded, setIsIpv4Expanded] = useState(true);
const [isIpv6Expanded, setIsIpv6Expanded] = useState(true);
const devName = iface.Name;
const dev = iface.Device;
const connectionSettings = iface?.MainConnection?.Settings || (createGhostConnectionSettings(devName, dev) as NMConnectionSettings);
const isManaged = iface && (!dev || is_managed(dev));
const toggleDevAction = (
<Switch id="network-interface-enable-device"
label={_("Enable device during installation")}
isReversed
isChecked={!!(dev?.ActiveConnection)}
onChange={(_event, enable) => enable
? connectInterface(model, iface, dev, connectionSettings)
: disconnectInterface(model, iface, dev)}
/>
);
const autoconnectSwitch = (
<Switch id="network-interface-connect-automatically"
label={_("Connect automatically on reboot")}
isReversed
isChecked={connectionSettings.connection.autoconnect}
onChange={(_event, checked) => toggleConnectionAutoconnect(model, dev, iface.MainConnection, connectionSettings, checked)}
/>
);
const isWireless = is_wireless(iface);
const activeAP = dev?.ActiveAccessPoint;
const ssid = activeAP ? activeAP.Ssid : null;
const joinNetButton = (
<Button variant="secondary">
{_("Join another network")}
</Button>
);
return (
<Card id="anaconda-network-interface">
<CardHeader actions={{ actions: toggleDevAction }}>
<CardTitle>{devName}</CardTitle>
</CardHeader>
<CardBody>
<DescriptionListWithAction dlTerm={_("Status")}
dlDescription={connectionState(dev, isManaged)}
actionButton={autoconnectSwitch}
/>
{ isWireless &&
<DescriptionListWithAction dlTerm={_("Network joined")}
dlDescription={ssid ?? _("Disconnected")}
actionButton={joinNetButton}
/>
}
{ isWireless && activeAP &&
<DescriptionListWithAction dlTerm={_("Security type")}
dlDescription={apSecurityFlagsToString(activeAP.Flags, activeAP.WpaFlags, activeAP.RsnFlags)}
/>
}
<DescriptionListWithAction dlTerm={_("IP settings")}
dlDescription={<>
<IPConfigExpandable toggleText={_("IPv4")}
onToggle={setIsIpv4Expanded}
isExpanded={isIpv4Expanded}
iface={iface}
settings={connectionSettings}
topic="ipv4"
/>
<IPConfigExpandable toggleText={_("IPv6")}
onToggle={setIsIpv6Expanded}
isExpanded={isIpv6Expanded}
iface={iface}
settings={connectionSettings}
topic="ipv6"
/>
</>}
/>
</CardBody>
</Card>
);
};

View file

@ -7,17 +7,18 @@
@include anaconda.embedded-page-shell;
}
// Main networking overview only: omit graphs and journal noise in the installer
#networking.anaconda #networking-graphs,
#networking.anaconda .cockpit-log-panel {
display: none;
}
@include anaconda.anaconda-body-overflow-hack;
// Remove padding above device name (header) in the detail view
#networking.anaconda #network-interface {
.pf-v6-c-card.pf-m-plain .pf-v6-c-card__header, .pf-v6-c-card.pf-m-plain > .pf-v6-c-card__title {
padding-block-start: 0;
#networking.anaconda {
// Add padding between each description list
.description-list-with-action:not(:first-of-type),
#ip-settings {
margin-block-start: var(--pf-t--global--spacer--lg);
}
// Add padding between expandable sections so edit buttons don't touch when
// sections are not expanded
.ip-expandable:not(:first-of-type) {
margin-block-start: var(--pf-t--global--spacer--md);
}
}

View file

@ -150,7 +150,7 @@ export const NetworkModal = ({ dialogError, help, idPrefix, title, onSubmit, chi
);
};
export const NetworkAction = ({ buttonText, iface, connectionSettings, type }) => {
export const NetworkAction = ({ buttonText, iface, connectionSettings, type, buttonVariant, isInline }) => {
const Dialogs = useDialogs();
const model = useContext(ModelContext);
@ -234,11 +234,14 @@ export const NetworkAction = ({ buttonText, iface, connectionSettings, type }) =
.catch(err => console.error("NetworkAction Dialog failed:", err)); // not-covered: OS error
}
const variant = buttonVariant ?? (!iface ? "secondary" : "link");
const inline = isInline ?? !!iface;
return (
<Button id={"networking-" + (!iface ? "add-" : "edit-") + type}
isInline={!!iface}
isInline={inline}
onClick={syn_click(model, show)}
variant={!iface ? "secondary" : "link"}>
variant={variant}>
{buttonText || _("edit")}
</Button>
);

View file

@ -980,7 +980,8 @@ export function NetworkManagerModel() {
],
props: {
AddressData: { conv: conv_Array(ip_address_from_nm), def: [] }
AddressData: { conv: conv_Array(ip_address_from_nm), def: [] },
Gateway: { def: "" },
}
};
@ -990,7 +991,8 @@ export function NetworkManagerModel() {
],
props: {
AddressData: { conv: conv_Array(ip_address_from_nm), def: [] }
AddressData: { conv: conv_Array(ip_address_from_nm), def: [] },
Gateway: { def: "" },
}
};
@ -1645,6 +1647,49 @@ export function array_join(elts, sep) {
return result;
}
export function active_connection_ipv4(dev) {
const con = dev.ActiveConnection;
const ip4config = con ? con.Ip4Config : dev.Ip4Config;
const parts = [];
if (ip4config) {
ip4config.AddressData.forEach(function (a) {
parts.push(a.address + "/" + a.prefix);
});
}
return parts;
}
export function active_connection_ipv6(dev, hide_link_local) {
const con = dev.ActiveConnection;
function is_ipv6_link_local(addr) {
return (addr.indexOf("fe8") === 0 ||
addr.indexOf("fe9") === 0 ||
addr.indexOf("fea") === 0 ||
addr.indexOf("feb") === 0);
}
const ip6config = con ? con.Ip6Config : dev.Ip6Config;
const parts = [];
if (ip6config) {
ip6config.AddressData.forEach(function (a) {
if (!(hide_link_local && is_ipv6_link_local(a.address)))
parts.push(a.address + "/" + a.prefix);
});
}
return parts;
}
export function active_connection_gateway(dev, topic) {
const con = dev.ActiveConnection;
const ipProp = topic === "ipv4" ? "Ip4Config" : "Ip6Config";
const ipconfig = con ? con[ipProp] : dev[ipProp];
return ipconfig?.Gateway || _("Not configured");
}
export function render_active_connection(dev, with_link, hide_link_local) {
const parts = [];
@ -1657,27 +1702,8 @@ export function render_active_connection(dev, with_link, hide_link_local) {
return fmt_to_fragments(_("Part of $0"), with_link ? render_interface_link(con.Group.Interface) : con.Group.Interface);
}
const ip4config = con ? con.Ip4Config : dev.Ip4Config;
if (ip4config) {
ip4config.AddressData.forEach(function (a) {
parts.push(a.address + "/" + a.prefix);
});
}
function is_ipv6_link_local(addr) {
return (addr.indexOf("fe8") === 0 ||
addr.indexOf("fe9") === 0 ||
addr.indexOf("fea") === 0 ||
addr.indexOf("feb") === 0);
}
const ip6config = con ? con.Ip6Config : dev.Ip6Config;
if (ip6config) {
ip6config.AddressData.forEach(function (a) {
if (!(hide_link_local && is_ipv6_link_local(a.address)))
parts.push(a.address + "/" + a.prefix);
});
}
parts.push(...active_connection_ipv4(dev));
parts.push(...active_connection_ipv6(dev, hide_link_local));
return parts.join(", ");
}
@ -1704,6 +1730,34 @@ export function complete_settings(settings, device) {
}
}
export function createGhostConnectionSettings(ifaceName, dev) {
const settings = {
connection: {
interface_name: ifaceName,
autoconnect: true,
autoconnect_priority: 0,
},
ipv4: {
method: "auto",
address_data: [],
dns_data: [],
dns_search: [],
route_data: [],
gateway: "",
},
ipv6: {
method: "auto",
address_data: [],
dns_data: [],
dns_search: [],
route_data: [],
gateway: "",
}
};
complete_settings(settings, dev);
return settings;
}
export function settings_applier(model, device, connection) {
/* If we have a connection, we can just update it.
* Otherwise if the settings has TYPE set, we can add
@ -2125,6 +2179,51 @@ export function is_wireless(iface) {
return iface.Device?.DeviceType === '802-11-wireless';
}
export function connectInterface(model, iface, dev, ghostSettings) {
if (!(iface.MainConnection || (dev && ghostSettings)))
return;
function modify() {
if (iface.MainConnection) {
return iface.MainConnection.activate(dev, null).catch(show_unexpected_error);
} else {
return dev.activate_with_settings(ghostSettings, null).catch(show_unexpected_error);
}
}
const dev_name = iface.Name;
with_checkpoint(model, modify,
{
devices: dev ? [dev] : [],
fail_text: fmt_to_fragments(_("Switching on $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{dev_name}</b>),
anyway_text: cockpit.format(_("Switch on $0"), dev_name)
});
}
export function disconnectInterface(model, iface, dev) {
if (!dev) {
console.log("Trying to switch off without a device?");
return;
}
function modify() {
return dev.disconnect().catch(show_unexpected_error);
}
const dev_name = iface.Name;
with_checkpoint(model, modify,
{
devices: [dev],
fail_text: fmt_to_fragments(_("Switching off $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{dev_name}</b>),
anyway_text: cockpit.format(_("Switch off $0"), dev_name)
});
}
export function toggleConnectionAutoconnect(model, dev, con, settings, autoconnect) {
settings.connection.autoconnect = autoconnect;
return settings_applier(model, dev, con)(settings);
}
export function init() {
cockpit.translate();
}

View file

@ -1,4 +1,4 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
import React from 'react';
export const ModelContext = React.createContext(undefined);
export const ModelContext = React.createContext(null);

View file

@ -46,15 +46,17 @@ import * as utils from "./utils.js";
import {
array_join,
choice_title,
complete_settings,
connectInterface,
connection_settings,
createGhostConnectionSettings,
disconnectInterface,
free_member_connection,
is_managed,
render_active_connection,
settings_applier,
show_error_dialog,
show_unexpected_error,
syn_click,
toggleConnectionAutoconnect,
with_checkpoint,
} from './interfaces.js';
import {
@ -283,7 +285,7 @@ export const NetworkInterfacePage = ({
if (iface.MainConnection) {
connectionSettings = iface.MainConnection.Settings;
} else {
ghostSettings = createGhostConnectionSettings();
ghostSettings = createGhostConnectionSettings(iface.Name, dev);
connectionSettings = ghostSettings;
}
}
@ -323,49 +325,6 @@ export const NetworkInterfacePage = ({
}
}
function connect() {
if (!(iface.MainConnection || (dev && ghostSettings)))
return;
function fail(error) {
show_unexpected_error(error);
}
function modify() {
if (iface.MainConnection) {
return iface.MainConnection.activate(dev, null).catch(fail);
} else {
return dev.activate_with_settings(ghostSettings, null).catch(fail);
}
}
with_checkpoint(model, modify,
{
devices: dev ? [dev] : [],
fail_text: fmt_to_fragments(_("Switching on $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{dev_name}</b>),
anyway_text: cockpit.format(_("Switch on $0"), dev_name)
});
}
function disconnect() {
if (!dev) {
console.log("Trying to switch off without a device?");
return;
}
function modify () {
return dev.disconnect()
.catch(error => show_unexpected_error(error));
}
with_checkpoint(model, modify,
{
devices: [dev],
fail_text: fmt_to_fragments(_("Switching off $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{dev_name}</b>),
anyway_text: cockpit.format(_("Switch off $0"), dev_name)
});
}
function renderDesc() {
let desc;
let cs;
@ -521,8 +480,7 @@ export const NetworkInterfacePage = ({
<DescriptionListDescription data-label="General">
<Checkbox id="autoreconnect" isDisabled={!privileged}
onChange={(_event, checked) => {
settings.connection.autoconnect = checked;
settings_applier(model, dev, con)(settings);
toggleConnectionAutoconnect(model, dev, con, settings, checked);
}}
isChecked={settings.connection.autoconnect}
label={_("Connect automatically")} />
@ -1170,30 +1128,6 @@ export const NetworkInterfacePage = ({
);
}
function createGhostConnectionSettings() {
const settings = {
connection: {
interface_name: iface.Name
},
ipv4: {
method: "auto",
address_data: [],
dns_data: [],
dns_search: [],
route_data: []
},
ipv6: {
method: "auto",
address_data: [],
dns_data: [],
dns_search: [],
route_data: []
}
};
complete_settings(settings, dev);
return settings;
}
/* Disable the On/Off button for interfaces that we don't know about at all,
and for devices that NM declares to be unavailable. Neither can be activated.
*/
@ -1207,7 +1141,9 @@ export const NetworkInterfacePage = ({
<Switch id="interface-switch"
isChecked={!!(dev && dev.ActiveConnection)}
isDisabled={!iface || (dev && dev.State === 20) || !privileged}
onChange={(_event, enable) => enable ? connect() : disconnect()}
onChange={(_event, enable) => enable
? connectInterface(model, iface, dev, ghostSettings)
: disconnectInterface(model, iface, dev)}
aria-label={_("Enable or disable the device")} />
</Privileged>
);

View file

@ -103,8 +103,7 @@ const App = () => {
return (
<ModelContext.Provider value={model}>
<WithDialogs key="networking-anaconda">
<AnacondaNetworkPage privileged={superuser.allowed}
operationInProgress={model.operationInProgress}
<AnacondaNetworkPage operationInProgress={model.operationInProgress}
interfaces={model.list_interfaces()} />
</WithDialogs>
</ModelContext.Provider>

View file

@ -10,22 +10,62 @@ const _ = cockpit.gettext;
type IPAddress = ipaddr.IPv4 | ipaddr.IPv6;
type NetworkManagerSettings = {
connection: {
id: string;
uuid?: string;
interface_name: string | undefined;
multi_connect?: number;
type: string
autoconnect_priority: number;
}
// Partial NetworkManager objects types
type NMIPMethod = "auto" | "manual" | "dhcp" | "link-local" | "ignore" | "shared" | "disabled";
type NMIPSettings = {
method: NMIPMethod;
address_data: {
address: string;
prefix: string;
}[];
dns_data: string[];
dns_search: string[];
route_data: unknown[];
gateway: string;
}
type NetworkManagerConnection = {
Settings: NetworkManagerSettings;
export type NMConnectionSettings = {
connection: {
interface_name: string | undefined;
autoconnect: boolean;
autoconnect_priority: number;
// Optional because createGhostConnectionSettings() doesn't set default values
id?: string;
uuid?: string;
multi_connect?: number;
type?: string;
};
ipv4?: NMIPSettings;
ipv6?: NMIPSettings;
}
export type NMConnection = {
Settings?: NMConnectionSettings;
Unsaved: boolean;
}
export type NMAccessPoint = {
Ssid: string;
Flags: number;
WpaFlags: number;
RsnFlags: number;
}
export type NMDevice = {
StateText: string;
// this is type_ActiveConnection
ActiveConnection: unknown;
ActiveAccessPoint: NMAccessPoint | null | undefined;
}
export type NMInterface = {
Name: string;
Device: NMDevice;
MainConnection: NMConnection | null;
}
/* NetworkManager specific data conversions and utility functions.
*/
@ -333,7 +373,7 @@ export function list_interfaces() {
// on top of this, remove the multiconnection settings and assign it directly
// to interface. This is done for Anaconda in order to avoid creating unexpected
// multiconnections when user modifies the connection during installation.
export function isNonPersistentMultiCon(connection: NetworkManagerConnection): boolean {
export function isNonPersistentMultiCon(connection: NMConnection): boolean {
const settings = connection?.Settings;
if (settings &&
settings.connection.interface_name === undefined &&
@ -347,7 +387,7 @@ export function isNonPersistentMultiCon(connection: NetworkManagerConnection): b
return false;
}
export function createNewConnSettings(settings: NetworkManagerSettings, iface_name: string) {
export function createNewConnSettings(settings: NMConnectionSettings, iface_name: string) {
const newSettings = {
...settings,
connection: {
@ -364,6 +404,61 @@ export function createNewConnSettings(settings: NetworkManagerSettings, iface_na
return newSettings;
}
const NM80211ApFlags = {
// access point has no special capabilities
NONE: 0x00000000,
// access point requires authentication and encryption (usually means WEP)
PRIVACY: 0x00000001,
} as const;
const NM80211ApSecurityFlags = {
// the access point has no special security requirements
NONE: 0x00000000,
// 40/64-bit WEP is supported for pairwise/unicast encryption
PAIR_WEP40: 0x00000001,
// 104/128-bit WEP is supported for pairwise/unicast encryption
PAIR_WEP104: 0x00000002,
// TKIP is supported for pairwise/unicast encryption
PAIR_TKIP: 0x00000004,
// AES/CCMP is supported for pairwise/unicast encryption
PAIR_CCMP: 0x00000008,
// 40/64-bit WEP is supported for group/broadcast encryption
GROUP_WEP40: 0x00000010,
// 104/128-bit WEP is supported for group/broadcast encryption
GROUP_WEP104: 0x00000020,
// TKIP is supported for group/broadcast encryption
GROUP_TKIP: 0x00000040,
// AES/CCMP is supported for group/broadcast encryption
GROUP_CCMP: 0x00000080,
// WPA/RSN Pre-Shared Key encryption is supported
KEY_MGMT_PSK: 0x00000100,
// 802.1x authentication and key management is supported
KEY_MGMT_802_1X: 0x00000200,
} as const;
// See NetworkManager examples for access point flags conversion:
// https://github.com/NetworkManager/NetworkManager/blob/1c71c568e7736ce2075fd0d53c387f2221bab832/examples/python/gi/show-wifi-networks.py#L64-L76
export function apSecurityFlagsToString(flags: number, wpaFlags: number, rsnFlags: number): string {
let str = "";
if ((flags & NM80211ApFlags.PRIVACY) && wpaFlags === 0 && rsnFlags === 0) {
str += " WEP";
}
if (wpaFlags !== 0) {
str += " WPA1";
}
if (rsnFlags !== 0) {
str += " WPA2";
}
if ((wpaFlags & NM80211ApSecurityFlags.KEY_MGMT_802_1X) || (rsnFlags & NM80211ApSecurityFlags.KEY_MGMT_802_1X)) {
str += " 802.1X";
}
if (str === "") {
str = _("Unsecured");
}
return str.trimStart();
}
export function debug(...args: Parameters<typeof console.debug>): void {
if (window.debugging === "all" || window.debugging?.includes("networkmanager")) // not-covered: debugging
console.debug("networkmanager:", ...args); // not-covered: debugging