/* jshint esversion: 2024, module: true */ import { SectionName, FieldType, DHCPMode, IPForward, IPv6PrivacyExtensions, LLMNROptions, MulticastDNS, DNSSECOptions, UseDomains, ClientIdentifier, RouteScope, RouteType, WakeOnLAN, PortType, DuplexMode, SLAACOptions, IPv6SendRAOptions, } from "./network-types.js"; /** * Base field type with standardized interface * @class BaseField */ class BaseField { #value; #type; #description; #options; /** * @param {*} value - Field value * @param {Symbol} type - Field type from FieldType enum * @param {string} description - Field description * @param {Object} options - Additional options (pattern, enum, etc.) */ constructor( value = null, type = FieldType.STRING, description = "", options = {}, ) { this.#value = value; this.#type = type; this.#description = description; this.#options = options; } /** * Get field value * @returns {*} */ get value() { return this.#value; } /** * Set field value * @param {*} newValue */ set value(newValue) { this.#value = newValue; } /** * Get field type * @returns {Symbol} */ get type() { return this.#type; } /** * Get field description * @returns {string} */ get description() { return this.#description; } /** * Get field options * @returns {Object} */ get options() { return { ...this.#options }; } /** * Validate field value * @returns {boolean} */ validate() { if ( this.#value === null || this.#value === undefined || this.#value === "" ) { return true; } // Handle boolean conversion for yes/no strings if (this.#type === FieldType.BOOLEAN && typeof this.#value === "string") { const normalized = this.#value.toLowerCase(); return ( normalized === "yes" || normalized === "no" || normalized === "true" || normalized === "false" ); } if (this.#options.pattern && typeof this.#value === "string") { return this.#options.pattern.test(this.#value); } if (this.#options.enum && this.#options.enum.length > 0) { return this.#options.enum.includes(this.#value); } return true; } /** * Convert to string representation * @returns {string} */ toString() { if (this.#value === null || this.#value === undefined) { return ""; } // Handle boolean conversion if (this.#type === FieldType.BOOLEAN) { if (typeof this.#value === "boolean") { return this.#value ? "yes" : "no"; } if (typeof this.#value === "string") { const normalized = this.#value.toLowerCase(); if (normalized === "true" || normalized === "false") { return normalized === "true" ? "yes" : "no"; } return normalized; // already yes/no } } return String(this.#value); } /** * Get boolean value * @returns {boolean|null} */ getBooleanValue() { if (this.#type !== FieldType.BOOLEAN) { return null; } if (this.#value === null || this.#value === undefined) { return null; } if (typeof this.#value === "boolean") { return this.#value; } if (typeof this.#value === "string") { const normalized = this.#value.toLowerCase(); return normalized === "yes" || normalized === "true"; } return Boolean(this.#value); } /** * Set boolean value * @param {boolean} boolValue */ setBooleanValue(boolValue) { if (this.#type === FieldType.BOOLEAN) { this.#value = boolValue; } } /** * Check if field has a value * @returns {boolean} */ hasValue() { return ( this.#value !== null && this.#value !== undefined && this.#value !== "" ); } } /** * MAC Address type * @class MACAddress */ class MACAddress extends BaseField { constructor(value = null) { super(value, FieldType.MAC_ADDRESS, "Hardware address (MAC)", { pattern: /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/, }); } } /** * IPv4 Address type * @class IPv4Address */ class IPv4Address extends BaseField { constructor(value = null) { super(value, FieldType.IPV4_ADDRESS, "IPv4 address", { pattern: /^(\d{1,3}\.){3}\d{1,3}$/, }); } } /** * IPv6 Address type * @class IPv6Address */ class IPv6Address extends BaseField { constructor(value = null) { super(value, FieldType.IPV6_ADDRESS, "IPv6 address", { pattern: /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/, }); } } /** * Boolean type with yes/no values * @class BooleanField */ class BooleanField extends BaseField { constructor(value = null) { super(value, FieldType.BOOLEAN, "Boolean (yes/no)"); } } /** * Port type * @class Port */ class Port extends BaseField { constructor(value = null) { super(value, FieldType.PORT, "Network port (1-65535)", { pattern: /^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/, }); } } /** * MTU type * @class MTU */ class MTU extends BaseField { constructor(value = null) { super(value, FieldType.MTU, "Maximum Transmission Unit", { pattern: /^[1-9][0-9]*$/, }); } } /** * [Match] section configuration * @class MatchSection */ class MatchSection { constructor() { this.MACAddress = new BaseField( null, FieldType.STRINGS, "Space-separated MAC addresses", ); this.OriginalName = new BaseField( null, FieldType.STRINGS, "Original interface names", ); this.Path = new BaseField(null, FieldType.STRINGS, "Device path patterns"); this.Driver = new BaseField(null, FieldType.STRINGS, "Driver names"); this.Type = new BaseField( null, FieldType.STRINGS, "Interface types (ether, wifi, etc.)", ); this.Name = new BaseField(null, FieldType.STRINGS, "Interface names"); this.Property = new BaseField(null, FieldType.STRING, "Device property"); this.Host = new BaseField(null, FieldType.STRING, "Host name"); this.Virtualization = new BaseField( null, FieldType.STRING, "Virtualization detection", ); this.KernelCommandLine = new BaseField( null, FieldType.STRING, "Kernel command line", ); this.Architecture = new BaseField( null, FieldType.STRING, "System architecture", ); } } /** * [Link] section configuration * @class LinkSection */ class LinkSection { constructor() { this.MACAddress = new MACAddress(); this.MTUBytes = new MTU(); this.BitsPerSecond = new BaseField( null, FieldType.NUMBER, "Link speed in bits per second", ); this.Duplex = new BaseField(null, FieldType.STRING, "Duplex mode", { enum: Object.values(DuplexMode), }); this.AutoNegotiation = new BooleanField(); this.WakeOnLan = new BaseField(null, FieldType.STRING, "Wake-on-LAN", { enum: Object.values(WakeOnLAN), }); this.Port = new BaseField(null, FieldType.STRING, "Port type", { enum: Object.values(PortType), }); this.Advertise = new BaseField( null, FieldType.STRINGS, "Advertised features", ); this.RxFlowControl = new BooleanField(); this.TxFlowControl = new BooleanField(); } } /** * [Network] section configuration * @class NetworkSection */ class NetworkSection { constructor() { this.Description = new BaseField( null, FieldType.STRING, "Interface description", ); this.DHCP = new BaseField(null, FieldType.DHCP_MODE, "DHCP client", { enum: Object.values(DHCPMode), }); this.DHCPServer = new BooleanField(); this.DNS = new BaseField(null, FieldType.IP_ADDRESSES, "DNS servers"); this.NTP = new BaseField(null, FieldType.IP_ADDRESSES, "NTP servers"); this.IPForward = new BaseField( null, FieldType.IP_FORWARD, "IP forwarding", { enum: Object.values(IPForward), }, ); this.IPv6PrivacyExtensions = new BaseField( null, FieldType.PRIVACY_EXTENSIONS, "IPv6 privacy extensions", { enum: Object.values(IPv6PrivacyExtensions), }, ); this.IPv6AcceptRA = new BooleanField(); this.LLMNR = new BaseField(null, FieldType.LLMNR, "LLMNR support", { enum: Object.values(LLMNROptions), }); this.MulticastDNS = new BaseField(null, FieldType.MDNS, "Multicast DNS", { enum: Object.values(MulticastDNS), }); this.DNSSEC = new BaseField(null, FieldType.DNSSEC, "DNSSEC support", { enum: Object.values(DNSSECOptions), }); this.Domains = new BaseField(null, FieldType.STRINGS, "DNS search domains"); this.ConfigureWithoutCarrier = new BooleanField(); this.IgnoreCarrierLoss = new BooleanField(); this.KeepConfiguration = new BaseField( null, FieldType.NUMBER, "Keep configuration time in seconds", ); this.LLDP = new BooleanField(); this.EmitLLDP = new BooleanField(); this.IPv6SendRA = new BooleanField(); } } /** * [DHCP] section configuration * @class DHCPSection */ class DHCPSection { constructor() { this.UseDNS = new BooleanField(); this.UseNTP = new BooleanField(); this.UseMTU = new BooleanField(); this.UseHostname = new BooleanField(); this.UseDomains = new BaseField( null, FieldType.USE_DOMAINS, "Use domains from DHCP", { enum: Object.values(UseDomains), }, ); this.ClientIdentifier = new BaseField( null, FieldType.CLIENT_IDENTIFIER, "DHCP client identifier", { enum: Object.values(ClientIdentifier), }, ); this.RouteMetric = new BaseField( null, FieldType.NUMBER, "Route metric for DHCP routes", ); this.UseRoutes = new BooleanField(); this.SendRelease = new BooleanField(); } } /** * [DHCPv4] section configuration * @class DHCPv4Section */ class DHCPv4Section { constructor() { this.ClientIdentifier = new BaseField( null, FieldType.CLIENT_IDENTIFIER, "DHCPv4 client identifier", { enum: Object.values(ClientIdentifier), }, ); this.UseDNS = new BooleanField(); this.UseNTP = new BooleanField(); this.UseMTU = new BooleanField(); this.UseHostname = new BooleanField(); this.UseDomains = new BaseField( null, FieldType.USE_DOMAINS, "Use domains from DHCPv4", { enum: Object.values(UseDomains), }, ); this.SendRelease = new BooleanField(); } } /** * [DHCPv6] section configuration * @class DHCPv6Section */ class DHCPv6Section { constructor() { this.UseDNS = new BooleanField(); this.UseNTP = new BooleanField(); this.UseHostname = new BooleanField(); this.UseDomains = new BaseField( null, FieldType.USE_DOMAINS, "Use domains from DHCPv6", { enum: Object.values(UseDomains), }, ); this.WithoutRA = new BooleanField(); this.UseAddress = new BooleanField(); } } /** * [IPv6AcceptRA] section configuration * @class IPv6AcceptRASection */ class IPv6AcceptRASection { constructor() { this.UseDNS = new BooleanField(); this.UseDomains = new BaseField( null, FieldType.USE_DOMAINS, "Use domains from RA", { enum: Object.values(UseDomains), }, ); this.UseAutonomousPrefix = new BooleanField(); this.UseOnLinkPrefix = new BooleanField(); this.UseRoutePrefix = new BooleanField(); this.RouteMetric = new BaseField( null, FieldType.NUMBER, "Route metric for RA routes", ); } } /** * [IPv6SendRA] section configuration * @class IPv6SendRASection */ class IPv6SendRASection { constructor() { this.RouterLifetimeSec = new BaseField( null, FieldType.NUMBER, "Router lifetime in seconds", ); this.EmitDNS = new BooleanField(); this.DNS = new BaseField( null, FieldType.IP_ADDRESSES, "DNS servers to advertise", ); this.EmitDomains = new BooleanField(); this.Domains = new BaseField( null, FieldType.STRINGS, "Domains to advertise", ); this.EmitNTP = new BooleanField(); this.NTP = new BaseField( null, FieldType.IP_ADDRESSES, "NTP servers to advertise", ); this.RouterPreference = new BaseField( null, FieldType.STRING, "Router preference", { enum: ["high", "medium", "low"], }, ); this.Managed = new BooleanField(); this.OtherInformation = new BooleanField(); } } /** * [SLAAC] section configuration * @class SLAACSection */ class SLAACSection { constructor() { this.UseDNS = new BooleanField(); this.UseDomains = new BaseField( null, FieldType.USE_DOMAINS, "Use domains from SLAAC", { enum: Object.values(UseDomains), }, ); this.UseAddress = new BooleanField(); this.RouteMetric = new BaseField( null, FieldType.NUMBER, "Route metric for SLAAC routes", ); this.Critical = new BooleanField(); this.PreferTemporaryAddress = new BooleanField(); this.UseAutonomousPrefix = new BooleanField(); this.UseOnLinkPrefix = new BooleanField(); this.UseRoutePrefix = new BooleanField(); } } /** * [Address] section configuration * @class AddressSection */ class AddressSection { constructor() { this.Address = new BaseField( null, FieldType.IP_PREFIX, "IP address with prefix", ); this.Peer = new BaseField(null, FieldType.IP_ADDRESS, "Peer address"); this.Broadcast = new BaseField( null, FieldType.IP_ADDRESS, "Broadcast address", ); this.Label = new BaseField(null, FieldType.STRING, "Address label"); this.Scope = new BaseField(null, FieldType.NUMBER, "Address scope"); this.Flags = new BaseField(null, FieldType.STRINGS, "Address flags"); this.Lifetime = new BaseField(null, FieldType.STRING, "Address lifetime"); } } /** * [Route] section configuration * @class RouteSection */ class RouteSection { constructor() { this.Gateway = new BaseField(null, FieldType.IP_ADDRESS, "Gateway address"); this.GatewayOnLink = new BooleanField(); this.Destination = new BaseField( null, FieldType.IP_PREFIX, "Destination prefix", ); this.Source = new BaseField(null, FieldType.IP_ADDRESS, "Source address"); this.PreferredSource = new BaseField( null, FieldType.IP_ADDRESS, "Preferred source address", ); this.Metric = new BaseField(null, FieldType.NUMBER, "Route metric"); this.Scope = new BaseField(null, FieldType.ROUTE_SCOPE, "Route scope", { enum: Object.values(RouteScope), }); this.Type = new BaseField(null, FieldType.ROUTE_TYPE, "Route type", { enum: Object.values(RouteType), }); this.InitialCongestionWindow = new BaseField( null, FieldType.NUMBER, "Initial congestion window", ); this.InitialAdvertisedReceiveWindow = new BaseField( null, FieldType.NUMBER, "Initial advertised receive window", ); this.Table = new BaseField(null, FieldType.NUMBER, "Routing table"); this.Protocol = new BaseField(null, FieldType.NUMBER, "Routing protocol"); } } /** * Complete network configuration * @class NetworkConfiguration */ class NetworkConfiguration { #match; #link; #network; #dhcp; #dhcpv4; #dhcpv6; #ipv6AcceptRA; #ipv6SendRA; #slaac; #address; #route; constructor() { this.#match = new MatchSection(); this.#link = new LinkSection(); this.#network = new NetworkSection(); this.#dhcp = new DHCPSection(); this.#dhcpv4 = new DHCPv4Section(); this.#dhcpv6 = new DHCPv6Section(); this.#ipv6AcceptRA = new IPv6AcceptRASection(); this.#ipv6SendRA = new IPv6SendRASection(); this.#slaac = new SLAACSection(); this.#address = []; this.#route = []; } // ... getter methods for all sections ... get Match() { return this.#match; } get Link() { return this.#link; } get Network() { return this.#network; } get DHCP() { return this.#dhcp; } get DHCPv4() { return this.#dhcpv4; } get DHCPv6() { return this.#dhcpv6; } get IPv6AcceptRA() { return this.#ipv6AcceptRA; } get IPv6SendRA() { return this.#ipv6SendRA; } get SLAAC() { return this.#slaac; } get Address() { return [...this.#address]; } get Route() { return [...this.#route]; } /** * Get schema for structured editor * @returns {Object} Schema definition */ getSchema() { return { Match: this.#getSectionSchema(this.#match), Link: this.#getSectionSchema(this.#link), Network: this.#getSectionSchema(this.#network), DHCP: this.#getSectionSchema(this.#dhcp), DHCPv4: this.#getSectionSchema(this.#dhcpv4), DHCPv6: this.#getSectionSchema(this.#dhcpv6), IPv6AcceptRA: this.#getSectionSchema(this.#ipv6AcceptRA), IPv6SendRA: this.#getSectionSchema(this.#ipv6SendRA), SLAAC: this.#getSectionSchema(this.#slaac), Address: this.#getArraySectionSchema(AddressSection, "Address"), Route: this.#getArraySectionSchema(RouteSection, "Route"), }; } #getSectionSchema(section) { const schema = {}; for (const [key, field] of Object.entries(section)) { schema[key] = { value: field.value, type: field.type, description: field.description, options: field.options, }; } return schema; } #getArraySectionSchema(SectionClass, sectionName) { const template = new SectionClass(); return { itemSchema: this.#getSectionSchema(template), items: this[sectionName].map((item) => this.#getSectionSchema(item)), }; } /** * Parse systemd network configuration from text * @static * @param {string} configText - Configuration file content * @returns {NetworkConfiguration} */ static fromSystemdConfiguration(configText) { const config = new NetworkConfiguration(); const lines = configText.split("\n"); let currentSection = null; let currentAddress = null; let currentRoute = null; for (const line of lines) { const trimmed = line.trim(); // Skip empty lines and comments if (!trimmed || trimmed.startsWith("#")) continue; // Section header const sectionMatch = trimmed.match(/^\[(\w+)\]$/i); if (sectionMatch) { const sectionName = sectionMatch[1].toLowerCase(); currentSection = config.#getSectionEnum(sectionName); // Start new array sections if (currentSection === SectionName.ADDRESS) { currentAddress = new AddressSection(); config.#address.push(currentAddress); } else if (currentSection === SectionName.ROUTE) { currentRoute = new RouteSection(); config.#route.push(currentRoute); } continue; } // Key-value pair const kvMatch = trimmed.match(/^(\w+)\s*=\s*(.+)$/i); if (kvMatch && currentSection) { const key = kvMatch[1]; const value = kvMatch[2]; config.#setValue( currentSection, key, value, currentAddress, currentRoute, ); } } return config; } /** * Get section enum from string * @private * @param {string} sectionName - Section name as string * @returns {Symbol} Section enum */ #getSectionEnum(sectionName) { const sectionMap = { match: SectionName.MATCH, link: SectionName.LINK, network: SectionName.NETWORK, dhcp: SectionName.DHCP, dhcpv4: SectionName.DHCPV4, dhcpv6: SectionName.DHCPV6, ipv6acceptra: SectionName.IPV6_ACCEPT_RA, ipv6sendra: SectionName.IPV6_SEND_RA, slaac: SectionName.SLAAC, address: SectionName.ADDRESS, route: SectionName.ROUTE, }; return sectionMap[sectionName] || null; } /** * Set configuration value * @private * @param {Symbol} section - Section enum * @param {string} key - Key name * @param {string} value - Value * @param {AddressSection} currentAddress - Current address section * @param {RouteSection} currentRoute - Current route section */ #setValue(section, key, value, currentAddress, currentRoute) { switch (section) { case SectionName.MATCH: this.#setMatchValue(key, value); break; case SectionName.LINK: this.#setLinkValue(key, value); break; case SectionName.NETWORK: this.#setNetworkValue(key, value); break; case SectionName.DHCP: this.#setDHCPValue(key, value); break; case SectionName.DHCPV4: this.#setDHCPv4Value(key, value); break; case SectionName.DHCPV6: this.#setDHCPv6Value(key, value); break; case SectionName.IPV6_ACCEPT_RA: this.#setIPv6AcceptRAValue(key, value); break; case SectionName.IPV6_SEND_RA: this.#setIPv6SendRAValue(key, value); break; case SectionName.SLAAC: this.#setSLAACValue(key, value); break; case SectionName.ADDRESS: if (currentAddress) { this.#setAddressValue(currentAddress, key, value); } break; case SectionName.ROUTE: if (currentRoute) { this.#setRouteValue(currentRoute, key, value); } break; } } #setMatchValue(key, value) { if (this.#match[key] !== undefined) { this.#match[key].value = value; } } #setLinkValue(key, value) { if (this.#link[key] !== undefined) { this.#link[key].value = value; } } #setNetworkValue(key, value) { if (this.#network[key] !== undefined) { this.#network[key].value = value; } } #setDHCPValue(key, value) { if (this.#dhcp[key] !== undefined) { this.#dhcp[key].value = value; } } #setDHCPv4Value(key, value) { if (this.#dhcpv4[key] !== undefined) { this.#dhcpv4[key].value = value; } } #setDHCPv6Value(key, value) { if (this.#dhcpv6[key] !== undefined) { this.#dhcpv6[key].value = value; } } #setIPv6AcceptRAValue(key, value) { if (this.#ipv6AcceptRA[key] !== undefined) { this.#ipv6AcceptRA[key].value = value; } } #setIPv6SendRAValue(key, value) { if (this.#ipv6SendRA[key] !== undefined) { this.#ipv6SendRA[key].value = value; } } #setSLAACValue(key, value) { if (this.#slaac[key] !== undefined) { this.#slaac[key].value = value; } } #setAddressValue(address, key, value) { if (address[key] !== undefined) { address[key].value = value; } } #setRouteValue(route, key, value) { if (route[key] !== undefined) { route[key].value = value; } } /** * Convert to systemd network configuration format * @returns {string} */ toSystemdConfiguration() { const sections = []; // Add all sections in order const sectionOrder = [ { name: "Match", section: this.#match }, { name: "Link", section: this.#link }, { name: "Network", section: this.#network }, { name: "DHCP", section: this.#dhcp }, { name: "DHCPv4", section: this.#dhcpv4 }, { name: "DHCPv6", section: this.#dhcpv6 }, { name: "IPv6AcceptRA", section: this.#ipv6AcceptRA }, { name: "IPv6SendRA", section: this.#ipv6SendRA }, { name: "SLAAC", section: this.#slaac }, ]; sectionOrder.forEach(({ name, section }) => { if (this.#hasSectionValues(section)) { sections.push(`[${name}]`); sections.push(...this.#formatSection(section)); } }); // [Address] sections this.#address.forEach((addr) => { if (this.#hasSectionValues(addr)) { sections.push("[Address]"); sections.push(...this.#formatSection(addr)); } }); // [Route] sections this.#route.forEach((route) => { if (this.#hasSectionValues(route)) { sections.push("[Route]"); sections.push(...this.#formatSection(route)); } }); return sections.length > 0 ? sections.join("\n") + "\n" : ""; } #hasSectionValues(section) { return Object.values(section).some((field) => field.hasValue()); } #formatSection(section) { const lines = []; for (const [key, field] of Object.entries(section)) { if (field.hasValue()) { lines.push(`${key}=${field.toString()}`); } } return lines; } } // Export classes export { BaseField, MACAddress, IPv4Address, IPv6Address, BooleanField, Port, MTU, MatchSection, LinkSection, NetworkSection, DHCPSection, DHCPv4Section, DHCPv6Section, IPv6AcceptRASection, IPv6SendRASection, SLAACSection, AddressSection, RouteSection, NetworkConfiguration, };