diff --git a/packages/admin-ui/src/pages/packages/components/Network/PortsByService.tsx b/packages/admin-ui/src/pages/packages/components/Network/PortsByService.tsx index 9144280df..619b34819 100644 --- a/packages/admin-ui/src/pages/packages/components/Network/PortsByService.tsx +++ b/packages/admin-ui/src/pages/packages/components/Network/PortsByService.tsx @@ -12,8 +12,23 @@ import { MdAdd, MdClose } from "react-icons/md"; import "./network.scss"; import { InstalledPackageData, PortMapping, PortProtocol } from "@dappnode/types"; -const maxPortNumber = 32768 - 1; -const maxEphemeralPortNumber = 65535; +const maxRegisteredPortNumber = 32768 - 1; +const maxRegisteredPortNumberPlusOne = maxRegisteredPortNumber + 1; +const maxPortNumber = 65535; +// Wireguard port is inside ephemeral range +const wireguardPort = 51820; + +interface PortStatusSummary { + container: { + inEphemeralRange: PortMapping[]; + overTheMax: PortMapping[]; + }; + + host: { + inEphemeralRange: PortMapping[]; + overTheMax: PortMapping[]; + }; +} export function PortsByService({ dnpName, @@ -114,45 +129,102 @@ export function PortsByService({ return conflictingPorts; } - function getPortsOverTheMax(): PortMapping[] { - return ports.filter( - ({ host, container, deletable }) => - (deletable && (container > maxPortNumber || (host && host > maxPortNumber))) || - (host && host > maxEphemeralPortNumber) + function getPortStatusSummary(): PortStatusSummary { + const isInEphemeralRange = (port: number) => + port > maxRegisteredPortNumber && port <= maxPortNumber && port !== wireguardPort; + + const isOverTheMax = (port: number) => port > maxPortNumber; + + return ports.reduce( + (acc, portMapping) => { + const { container, host, deletable } = portMapping; + + if (!deletable) return acc; + + if (isOverTheMax(container)) { + acc.container.overTheMax.push(portMapping); + } else if (isInEphemeralRange(container)) { + acc.container.inEphemeralRange.push(portMapping); + } + + if (host === undefined) return acc; + + if (isOverTheMax(host)) { + acc.host.overTheMax.push(portMapping); + } else if (isInEphemeralRange(host)) { + acc.host.inEphemeralRange.push(portMapping); + } + + return acc; + }, + { + container: { + inEphemeralRange: [], + overTheMax: [] + }, + host: { + inEphemeralRange: [], + overTheMax: [] + } + } ); } const areNewMappingsInvalid = ports.some( ({ container, protocol, deletable }) => deletable && (!container || !protocol) ); + const duplicatedContainerPorts = getDuplicatedContainerPorts(); const duplicatedHostPorts = getDuplicatedHostPorts(); const conflictingPorts = getConflictingPorts(); - const portsOverTheMax = getPortsOverTheMax(); + const portStatusSummary = getPortStatusSummary(); const arePortsTheSame = portsToId(portsFromDnp) === portsToId(ports); - // Aggregate error messages as an array of strings + // Aggregate error & warning messages as an array of strings const errors: string[] = []; + const warnings: string[] = []; for (const duplicatedHostPort of duplicatedHostPorts) errors.push( - `Duplicated mapping for host port ${duplicatedHostPort.host}/${duplicatedHostPort.protocol}. Each host port can only be mapped once.` + `❌ Duplicated mapping for host port ${duplicatedHostPort.host}/${duplicatedHostPort.protocol}. Each host port can only be mapped once.` ); for (const duplicatedContainerPort of duplicatedContainerPorts) errors.push( - `Duplicated mapping for package port ${duplicatedContainerPort.container}/${duplicatedContainerPort.protocol}. Each package port can only be mapped once.` + `❌ Duplicated mapping for package port ${duplicatedContainerPort.container}/${duplicatedContainerPort.protocol}. Each package port can only be mapped once.` ); for (const conflictingPort of conflictingPorts) { const portName = `${conflictingPort.host}/${conflictingPort.protocol}`; const ownerName = prettyDnpName(conflictingPort.owner); - errors.push(`Port ${portName} is already mapped by the DAppNode Package ${ownerName}`); + errors.push(`❌ Port ${portName} is already mapped by the DAppNode Package ${ownerName}`); } - for (const portOverTheMax of portsOverTheMax) + // Adjusting the loops for error and warning outputs accordingly + portStatusSummary.container.overTheMax.forEach((port) => { + errors.push( + `❌ Container port mapping ${port.container}/${port.protocol} exceeds the maximum allowed port number of ${maxPortNumber} and can't be used.` + ); + }); + + portStatusSummary.host.overTheMax.forEach((port) => { errors.push( - `Host port mapping ${portOverTheMax.host}/${portOverTheMax.protocol} is in the ephemeral port range (32768-65535). It must be avoided.` + `❌ Host port mapping ${port.host}/${port.protocol} exceeds the maximum allowed port number of ${maxPortNumber} and can't be used.` ); + }); + + portStatusSummary.container.inEphemeralRange.forEach((port) => { + warnings.push( + `⚠️ Package Port mapping ${port.container}/${ + port.protocol + } is in the ephemeral port range (${maxRegisteredPortNumber + 1}-${maxPortNumber}).` + ); + }); + + portStatusSummary.host.inEphemeralRange.forEach((port) => { + warnings.push( + `⚠️ Host Port mapping ${port.host}/${port.protocol} is in the ephemeral port range (${maxRegisteredPortNumberPlusOne}-${maxPortNumber}).` + ); + }); // Aggregate conditions to disable the update const disableUpdate = Boolean( @@ -160,9 +232,10 @@ export function PortsByService({ duplicatedContainerPorts.length > 0 || duplicatedHostPorts.length > 0 || conflictingPorts.length > 0 || - portsOverTheMax.length > 0 || arePortsTheSame || - updating + updating || + portStatusSummary.container.overTheMax.length > 0 || + portStatusSummary.host.overTheMax.length > 0 ); return ( @@ -230,6 +303,10 @@ export function PortsByService({ ))} + {warnings.map((warning) => ( +
{warning}
+ ))} +