/* jshint esversion: 2024, module: true */ import { BooleanYesNo, ClientIdentifier, DHCPMode, DNSSECOptions, DuplexMode, FieldType, IPForward, IPv6PrivacyExtensions, LLMNROptions, MulticastDNS, PortType, RouteScope, RouteType, UseDomains, WakeOnLAN, } 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; } 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() { return this.#value !== null ? String(this.#value) : ""; } /** * 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}$/, }); } } /** * 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 BooleanYesNo(); 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 BooleanYesNo(); this.TxFlowControl = new BooleanYesNo(); } } /** * [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 BooleanYesNo(); 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 BooleanYesNo(); 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 BooleanYesNo(); this.IgnoreCarrierLoss = new BooleanYesNo(); this.KeepConfiguration = new BaseField( null, FieldType.NUMBER, "Keep configuration time in seconds", ); this.LLDP = new BooleanYesNo(); this.EmitLLDP = new BooleanYesNo(); } } /** * [DHCP] section configuration * @class DHCPSection */ class DHCPSection { constructor() { this.UseDNS = new BooleanYesNo(); this.UseNTP = new BooleanYesNo(); this.UseMTU = new BooleanYesNo(); this.UseHostname = new BooleanYesNo(); 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 BooleanYesNo(); this.SendRelease = new BooleanYesNo(); } } /** * [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 BooleanYesNo(); this.UseNTP = new BooleanYesNo(); this.UseMTU = new BooleanYesNo(); this.UseHostname = new BooleanYesNo(); this.UseDomains = new BaseField( null, FieldType.USE_DOMAINS, "Use domains from DHCPv4", { enum: Object.values(UseDomains), }, ); this.SendRelease = new BooleanYesNo(); } } /** * [DHCPv6] section configuration * @class DHCPv6Section */ class DHCPv6Section { constructor() { this.UseDNS = new BooleanYesNo(); this.UseNTP = new BooleanYesNo(); this.UseHostname = new BooleanYesNo(); this.UseDomains = new BaseField( null, FieldType.USE_DOMAINS, "Use domains from DHCPv6", { enum: Object.values(UseDomains), }, ); this.WithoutRA = new BooleanYesNo(); this.UseAddress = new BooleanYesNo(); } } /** * [IPv6AcceptRA] section configuration * @class IPv6AcceptRASection */ class IPv6AcceptRASection { constructor() { this.UseDNS = new BooleanYesNo(); this.UseDomains = new BaseField( null, FieldType.USE_DOMAINS, "Use domains from RA", { enum: Object.values(UseDomains), }, ); this.UseAutonomousPrefix = new BooleanYesNo(); this.UseOnLinkPrefix = new BooleanYesNo(); this.UseRoutePrefix = new BooleanYesNo(); this.RouteMetric = new BaseField( null, FieldType.NUMBER, "Route metric for RA routes", ); } } /** * [SLAAC] section configuration * @class SLAACSection */ class SLAACSection { constructor() { this.UseDNS = new BooleanYesNo(); this.UseDomains = new BaseField( null, FieldType.USE_DOMAINS, "Use domains from SLAAC", { enum: Object.values(UseDomains), }, ); this.UseAddress = new BooleanYesNo(); this.RouteMetric = new BaseField( null, FieldType.NUMBER, "Route metric for SLAAC routes", ); this.Critical = new BooleanYesNo(); this.PreferTemporaryAddress = new BooleanYesNo(); this.UseAutonomousPrefix = new BooleanYesNo(); this.UseOnLinkPrefix = new BooleanYesNo(); this.UseRoutePrefix = new BooleanYesNo(); } } /** * [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 BooleanYesNo(); 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; #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.#slaac = new SLAACSection(); this.#address = []; this.#route = []; } /** * Get Match section * @returns {MatchSection} */ get Match() { return this.#match; } /** * Get Link section * @returns {LinkSection} */ get Link() { return this.#link; } /** * Get Network section * @returns {NetworkSection} */ get Network() { return this.#network; } /** * Get DHCP section * @returns {DHCPSection} */ get DHCP() { return this.#dhcp; } /** * Get DHCPv4 section * @returns {DHCPv4Section} */ get DHCPv4() { return this.#dhcpv4; } /** * Get DHCPv6 section * @returns {DHCPv6Section} */ get DHCPv6() { return this.#dhcpv6; } /** * Get IPv6AcceptRA section * @returns {IPv6AcceptRASection} */ get IPv6AcceptRA() { return this.#ipv6AcceptRA; } /** * Get SLAAC section * @returns {SLAACSection} */ get SLAAC() { return this.#slaac; } /** * Get Address sections * @returns {Array} */ get Address() { return [...this.#address]; } /** * Get Route sections * @returns {Array} */ 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), SLAAC: this.#getSectionSchema(this.#slaac), Address: this.#getArraySectionSchema(AddressSection, "Address"), Route: this.#getArraySectionSchema(RouteSection, "Route"), }; } /** * Get schema for a single section * @private * @param {Object} section - Section instance * @returns {Object} Section schema */ #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; } /** * Get schema for array sections (Address, Route) * @private * @param {Class} SectionClass - Section class * @param {string} sectionName - Section name * @returns {Object} Array section 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+)\]$/); if (sectionMatch) { currentSection = sectionMatch[1].toLowerCase(); // Start new array sections if (currentSection === "address") { currentAddress = new AddressSection(); config.#address.push(currentAddress); } else if (currentSection === "route") { currentRoute = new RouteSection(); config.#route.push(currentRoute); } continue; } // Key-value pair const kvMatch = trimmed.match(/^(\w+)\s*=\s*(.+)$/); if (kvMatch && currentSection) { const key = kvMatch[1]; const value = kvMatch[2]; config.#setValue( currentSection, key, value, currentAddress, currentRoute, ); } } return config; } /** * Set configuration value * @private * @param {string} section - Section name * @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 "match": this.#setMatchValue(key, value); break; case "link": this.#setLinkValue(key, value); break; case "network": this.#setNetworkValue(key, value); break; case "dhcp": this.#setDHCPValue(key, value); break; case "dhcpv4": this.#setDHCPv4Value(key, value); break; case "dhcpv6": this.#setDHCPv6Value(key, value); break; case "ipv6acceptra": this.#setIPv6AcceptRAValue(key, value); break; case "slaac": this.#setSLAACValue(key, value); break; case "address": if (currentAddress) { this.#setAddressValue(currentAddress, key, value); } break; case "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; } } #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 = []; // [Match] section if (this.#hasSectionValues(this.#match)) { sections.push("[Match]"); sections.push(...this.#formatSection(this.#match)); } // [Link] section if (this.#hasSectionValues(this.#link)) { sections.push("[Link]"); sections.push(...this.#formatSection(this.#link)); } // [Network] section if (this.#hasSectionValues(this.#network)) { sections.push("[Network]"); sections.push(...this.#formatSection(this.#network)); } // [DHCP] section if (this.#hasSectionValues(this.#dhcp)) { sections.push("[DHCP]"); sections.push(...this.#formatSection(this.#dhcp)); } // [DHCPv4] section if (this.#hasSectionValues(this.#dhcpv4)) { sections.push("[DHCPv4]"); sections.push(...this.#formatSection(this.#dhcpv4)); } // [DHCPv6] section if (this.#hasSectionValues(this.#dhcpv6)) { sections.push("[DHCPv6]"); sections.push(...this.#formatSection(this.#dhcpv6)); } // [IPv6AcceptRA] section if (this.#hasSectionValues(this.#ipv6AcceptRA)) { sections.push("[IPv6AcceptRA]"); sections.push(...this.#formatSection(this.#ipv6AcceptRA)); } // [SLAAC] section if (this.#hasSectionValues(this.#slaac)) { sections.push("[SLAAC]"); sections.push(...this.#formatSection(this.#slaac)); } // [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, BooleanYesNo, Port, MTU, MatchSection, LinkSection, NetworkSection, DHCPSection, DHCPv4Section, DHCPv6Section, IPv6AcceptRASection, SLAACSection, AddressSection, RouteSection, NetworkConfiguration, };