/* * Copyright (c) 2025 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonVPN is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ using System.Net; using System.Net.Sockets; using ProtonVPN.Common.Core.Extensions; namespace ProtonVPN.Common.Core.Networking; public readonly record struct NetworkAddress { private const int MIN_SUBNET = 0; private const int MAX_SUBNET_IPV4 = 32; private const int MAX_SUBNET_IPV6 = 128; private const string DEFAULT_IPV4_MASK = "255.255.255.255"; public static NetworkAddress None => new(IPAddress.None); public IPAddress Ip { get; } public int? Subnet { get; } public bool IsIpV4 => Ip.AddressFamily == AddressFamily.InterNetwork; public bool IsIpV6 => Ip.AddressFamily == AddressFamily.InterNetworkV6; public string FormattedAddress => ToString(); public bool IsSingleIp => !Subnet.HasValue || Subnet == (IsIpV4 ? MAX_SUBNET_IPV4 : MAX_SUBNET_IPV6); public NetworkAddress(IPAddress ip) { Ip = ip; } private NetworkAddress(IPAddress ip, int? subnet = null) : this(ip) { Subnet = subnet; } public static bool TryParse(string? rawAddress, out NetworkAddress networkAddress) { networkAddress = None; try { if (string.IsNullOrWhiteSpace(rawAddress)) { throw new ArgumentException("Address cannot be null or empty.", nameof(rawAddress)); } // Split IP and CIDR subnet string[] parts = rawAddress.Trim().Split('/'); if (!IPAddress.TryParse(parts[0], out IPAddress? ip)) { throw new FormatException("Invalid IP address format."); } // Confirm the given IPv4 address is well formatted (#.#.#.#) if (ip.AddressFamily == AddressFamily.InterNetwork && parts[0] != ip.ToString()) { throw new FormatException("Invalid IPv4 address format."); } // Confirm there are 2 parts at most ( or /) if (parts.Length > 2) { throw new FormatException("Invalid CIDR notation format."); } // Confirm subnet value (if any) is in range int? subnet = null; if (parts.Length == 2) { Range subnetRange = ip.AddressFamily == AddressFamily.InterNetwork ? new Range(MIN_SUBNET, MAX_SUBNET_IPV4) : new Range(MIN_SUBNET, MAX_SUBNET_IPV6); if (!int.TryParse(parts[1], out int subnetValue) || !subnetRange.Contains(subnetValue)) { throw new FormatException("Invalid subnet value."); } subnet = subnetValue; } networkAddress = new NetworkAddress(ip, subnet); return true; } catch { return false; } } public string GetSubnetMaskString() { if (!IsIpV4) { throw new InvalidOperationException("Subnet mask conversion is only supported for IPv4 addresses."); } if (!Subnet.HasValue) { return DEFAULT_IPV4_MASK; } int cidr = Subnet.Value; uint mask = cidr == 0 ? 0 : 0xFFFFFFFF << (32 - cidr); byte[] bytes = [ (byte)(mask >> 24), (byte)(mask >> 16), (byte)(mask >> 8), (byte)mask ]; return string.Join(".", bytes); } public bool IsGlobalUnicastAddress() { byte[] bytes = Ip.GetAddressBytes(); return Ip.AddressFamily == AddressFamily.InterNetworkV6 && (bytes[0] & 0xE0) == 0x20; } public override string ToString() { return IsSingleIp ? Ip.ToString() : $"{Ip}/{Subnet}"; } }