/* jshint esversion: 2024, module: true */ /** * Systemd Network Configuration Parser * Based on systemd.network(5) documentation * @module SystemdNetwork */ /** * MAC Address type * @class MACAddress */ class MACAddress { /** * @param {string} value - MAC address value * @param {string} description - Description */ constructor(value = '', description = 'Hardware address') { this.value = value; this.type = 'mac-address'; this.description = description; this.pattern = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/; } /** * Validate MAC address format * @returns {boolean} */ validate() { return this.pattern.test(this.value); } toString() { return this.value; } } /** * IPv4 Address type * @class IPv4Address */ class IPv4Address { /** * @param {string} value - IPv4 address * @param {string} description - Description */ constructor(value = '', description = 'IPv4 address') { this.value = value; this.type = 'ipv4-address'; this.description = description; this.pattern = /^(\d{1,3}\.){3}\d{1,3}$/; } validate() { return this.pattern.test(this.value); } toString() { return this.value; } } /** * IPv6 Address type * @class IPv6Address */ class IPv6Address { /** * @param {string} value - IPv6 address * @param {string} description - Description */ constructor(value = '', description = 'IPv6 address') { this.value = value; this.type = 'ipv6-address'; this.description = description; this.pattern = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/; } validate() { return this.pattern.test(this.value); } toString() { return this.value; } } /** * [Match] section configuration * @class MatchSection */ class MatchSection { constructor() { /** @type {MACAddress[]} */ this.MACAddress = []; /** @type {string[]} */ this.OriginalName = []; /** @type {string[]} */ this.Path = []; /** @type {string[]} */ this.Driver = []; /** @type {string[]} */ this.Type = []; /** @type {string[]} */ this.Name = []; /** @type {string} */ this.Property = ''; /** @type {string} */ this.Host = ''; /** @type {string} */ this.Virtualization = ''; /** @type {string} */ this.KernelCommandLine = ''; /** @type {string} */ this.Architecture = ''; } } /** * [Link] section configuration * @class LinkSection */ class LinkSection { constructor() { /** @type {string} */ this.MACAddress = ''; /** @type {string} */ this.MTUBytes = ''; /** @type {number} */ this.BitsPerSecond = 0; /** @type {string} */ this.Duplex = ''; /** @type {string} */ this.AutoNegotiation = ''; /** @type {string} */ this.WakeOnLan = ''; /** @type {string} */ this.Port = ''; /** @type {string} */ this.Advertise = ''; /** @type {string} */ this.RxFlowControl = ''; /** @type {string} */ this.TxFlowControl = ''; } } /** * [Network] section configuration * @class NetworkSection */ class NetworkSection { constructor() { /** @type {string} */ this.Description = ''; /** @type {string[]} */ this.DHCP = []; // 'yes', 'no', 'ipv4', 'ipv6' /** @type {boolean} */ this.DHCPServer = false; /** @type {string[]} */ this.DNS = []; /** @type {string[]} */ this.NTP = []; /** @type {string[]} */ this.IPForward = []; // 'yes', 'no', 'ipv4', 'ipv6' /** @type {string} */ this.IPv6PrivacyExtensions = ''; // 'yes', 'no', 'prefer-public' /** @type {string} */ this.IPv6AcceptRA = ''; // 'yes', 'no' /** @type {string} */ this.LLMNR = ''; // 'yes', 'no', 'resolve' /** @type {string} */ this.MulticastDNS = ''; // 'yes', 'no', 'resolve' /** @type {string} */ this.DNSSEC = ''; // 'yes', 'no', 'allow-downgrade' /** @type {string[]} */ this.Domains = []; /** @type {string} */ this.ConfigureWithoutCarrier = ''; // 'yes', 'no' /** @type {string} */ this.IgnoreCarrierLoss = ''; // 'yes', 'no' /** @type {number} */ this.KeepConfiguration = 0; // seconds } } /** * [DHCP] section configuration * @class DHCPSection */ class DHCPSection { constructor() { /** @type {string} */ this.UseDNS = ''; // 'yes', 'no' /** @type {string} */ this.UseNTP = ''; // 'yes', 'no' /** @type {string} */ this.UseMTU = ''; // 'yes', 'no' /** @type {string} */ this.UseHostname = ''; // 'yes', 'no' /** @type {string} */ this.UseDomains = ''; // 'yes', 'no', 'route' /** @type {string} */ this.ClientIdentifier = ''; // 'mac', 'duid' /** @type {string} */ this.RouteMetric = ''; } } /** * [Address] section configuration * @class AddressSection */ class AddressSection { constructor() { /** @type {string} */ this.Address = ''; // IP address with prefix /** @type {string} */ this.Peer = ''; // Peer address /** @type {string} */ this.Broadcast = ''; // Broadcast address /** @type {string} */ this.Label = ''; // Address label /** @type {number} */ this.Scope = 0; // Address scope /** @type {string} */ this.Flags = ''; // Address flags } } /** * [Route] section configuration * @class RouteSection */ class RouteSection { constructor() { /** @type {string} */ this.Gateway = ''; // Gateway address /** @type {string} */ this.GatewayOnLink = ''; // 'yes', 'no' /** @type {string} */ this.Destination = ''; // Destination prefix /** @type {string} */ this.Source = ''; // Source address /** @type {string} */ this.PreferredSource = ''; // Preferred source address /** @type {number} */ this.Metric = 1024; // Route metric /** @type {string} */ this.Scope = ''; // 'global', 'link', 'host' /** @type {string} */ this.Type = ''; // 'unicast', 'local', 'broadcast', etc. } } /** * Complete network configuration * @class NetworkConfiguration */ class NetworkConfiguration { constructor() { /** @type {MatchSection} */ this.Match = new MatchSection(); /** @type {LinkSection} */ this.Link = new LinkSection(); /** @type {NetworkSection} */ this.Network = new NetworkSection(); /** @type {DHCPSection} */ this.DHCP = new DHCPSection(); /** @type {AddressSection[]} */ this.Address = []; /** @type {RouteSection[]} */ this.Route = []; } /** * Parse systemd network configuration from text * @param {string} configText - Configuration file content * @returns {NetworkConfiguration} */ static fromSystemdConfiguration(configText) { const config = new NetworkConfiguration(); const lines = configText.split('\n'); let currentSection = 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(); 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); } } return config; } /** * Set configuration value * @private * @param {string} section - Section name * @param {string} key - Key name * @param {string} value - Value */ _setValue(section, key, value) { 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 'address': // Handle multiple address sections if (!this.Address.length) this.Address.push(new AddressSection()); this._setAddressValue(this.Address[this.Address.length - 1], key, value); break; case 'route': // Handle multiple route sections if (!this.Route.length) this.Route.push(new RouteSection()); this._setRouteValue(this.Route[this.Route.length - 1], key, value); break; } } _setMatchValue(key, value) { const match = this.Match; switch (key) { case 'MACAddress': match.MACAddress = value.split(' '); break; case 'Name': match.Name = value.split(' '); break; case 'Driver': match.Driver = value.split(' '); break; case 'Path': match.Path = value.split(' '); break; case 'Type': match.Type = value.split(' '); break; default: match[key] = value; } } _setLinkValue(key, value) { this.Link[key] = value; } _setNetworkValue(key, value) { const network = this.Network; switch (key) { case 'DNS': case 'NTP': case 'Domains': case 'DHCP': case 'IPForward': network[key] = value.split(' '); break; default: network[key] = value; } } _setDHCPValue(key, value) { this.DHCP[key] = value; } _setAddressValue(address, key, value) { address[key] = value; } _setRouteValue(route, key, value) { route[key] = value; } /** * Convert to systemd network configuration format * @returns {string} */ toSystemdConfiguration() { const sections = []; // [Match] section if (this._hasMatchValues()) { sections.push('[Match]'); sections.push(...this._formatMatchSection()); } // [Link] section if (this._hasLinkValues()) { sections.push('[Link]'); sections.push(...this._formatLinkSection()); } // [Network] section if (this._hasNetworkValues()) { sections.push('[Network]'); sections.push(...this._formatNetworkSection()); } // [DHCP] section if (this._hasDHCPValues()) { sections.push('[DHCP]'); sections.push(...this._formatDHCPSection()); } // [Address] sections this.Address.forEach((addr, index) => { if (this._hasAddressValues(addr)) { sections.push('[Address]'); sections.push(...this._formatAddressSection(addr)); } }); // [Route] sections this.Route.forEach((route, index) => { if (this._hasRouteValues(route)) { sections.push('[Route]'); sections.push(...this._formatRouteSection(route)); } }); return sections.join('\n') + '\n'; } // Helper methods to check if sections have values _hasMatchValues() { return Object.values(this.Match).some(val => val && (Array.isArray(val) ? val.length > 0 : val !== '') ); } _hasLinkValues() { return Object.values(this.Link).some(val => val && val !== ''); } _hasNetworkValues() { return Object.values(this.Network).some(val => val && (Array.isArray(val) ? val.length > 0 : val !== '') ); } _hasDHCPValues() { return Object.values(this.DHCP).some(val => val && val !== ''); } _hasAddressValues(addr) { return Object.values(addr).some(val => val && val !== ''); } _hasRouteValues(route) { return Object.values(route).some(val => val && val !== ''); } // Formatting methods _formatMatchSection() { const lines = []; const match = this.Match; if (match.MACAddress.length) lines.push(`MACAddress=${match.MACAddress.join(' ')}`); if (match.Name.length) lines.push(`Name=${match.Name.join(' ')}`); if (match.Driver.length) lines.push(`Driver=${match.Driver.join(' ')}`); if (match.Path.length) lines.push(`Path=${match.Path.join(' ')}`); if (match.Type.length) lines.push(`Type=${match.Type.join(' ')}`); if (match.Property) lines.push(`Property=${match.Property}`); return lines; } _formatLinkSection() { const lines = []; const link = this.Link; Object.entries(link).forEach(([key, value]) => { if (value && value !== '') lines.push(`${key}=${value}`); }); return lines; } _formatNetworkSection() { const lines = []; const network = this.Network; if (network.Description) lines.push(`Description=${network.Description}`); if (network.DHCP.length) lines.push(`DHCP=${network.DHCP.join(' ')}`); if (network.DNS.length) lines.push(`DNS=${network.DNS.join(' ')}`); if (network.NTP.length) lines.push(`NTP=${network.NTP.join(' ')}`); if (network.IPForward.length) lines.push(`IPForward=${network.IPForward.join(' ')}`); if (network.IPv6PrivacyExtensions) lines.push(`IPv6PrivacyExtensions=${network.IPv6PrivacyExtensions}`); if (network.LLMNR) lines.push(`LLMNR=${network.LLMNR}`); return lines; } _formatDHCPSection() { const lines = []; const dhcp = this.DHCP; Object.entries(dhcp).forEach(([key, value]) => { if (value && value !== '') lines.push(`${key}=${value}`); }); return lines; } _formatAddressSection(addr) { const lines = []; Object.entries(addr).forEach(([key, value]) => { if (value && value !== '') lines.push(`${key}=${value}`); }); return lines; } _formatRouteSection(route) { const lines = []; Object.entries(route).forEach(([key, value]) => { if (value && value !== '') lines.push(`${key}=${value}`); }); return lines; } } // Export classes export { MACAddress, IPv4Address, IPv6Address, MatchSection, LinkSection, NetworkSection, DHCPSection, AddressSection, RouteSection, NetworkConfiguration };