/* jshint esversion: 2024, module: true */ /** * Systemd Network Configuration Parser * Based on systemd.network(5) documentation * @module SystemdNetwork */ /** * Base field type with standardized interface * @class BaseField */ class BaseField { /** * @param {*} value - Field value * @param {string} type - Field type * @param {string} description - Field description * @param {Object} options - Additional options (pattern, enum, etc.) */ constructor(value = null, type = 'string', description = '', options = {}) { this.value = value; this.type = type; this.description = description; this.options = options; } /** * Validate field value * @returns {boolean} */ validate() { if (this.value === null || this.value === undefined) 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) : ''; } } /** * MAC Address type * @class MACAddress */ class MACAddress extends BaseField { constructor(value = null) { super(value, '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, '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, '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 BooleanYesNo */ class BooleanYesNo extends BaseField { constructor(value = null) { super(value, 'boolean', 'Boolean (yes/no)', { enum: ['yes', 'no'] }); } } /** * Port type * @class Port */ class Port extends BaseField { constructor(value = null) { super(value, '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, 'mtu', 'Maximum Transmission Unit', { pattern: /^[1-9][0-9]*$/ }); } } /** * [Match] section configuration * @class MatchSection */ class MatchSection { constructor() { this.MACAddress = new BaseField(null, 'mac-addresses', 'Space-separated MAC addresses'); this.OriginalName = new BaseField(null, 'strings', 'Original interface names'); this.Path = new BaseField(null, 'strings', 'Device path patterns'); this.Driver = new BaseField(null, 'strings', 'Driver names'); this.Type = new BaseField(null, 'strings', 'Interface types (ether, wifi, etc.)'); this.Name = new BaseField(null, 'strings', 'Interface names'); this.Property = new BaseField(null, 'string', 'Device property'); this.Host = new BaseField(null, 'string', 'Host name'); this.Virtualization = new BaseField(null, 'string', 'Virtualization detection'); this.KernelCommandLine = new BaseField(null, 'string', 'Kernel command line'); this.Architecture = new BaseField(null, 'string', 'System architecture'); } } /** * [Link] section configuration * @class LinkSection */ class LinkSection { constructor() { this.MACAddress = new MACAddress(); this.MTUBytes = new MTU(); this.BitsPerSecond = new BaseField(null, 'number', 'Link speed in bits per second'); this.Duplex = new BaseField(null, 'string', 'Duplex mode', { enum: ['half', 'full'] }); this.AutoNegotiation = new BooleanYesNo(); this.WakeOnLan = new BaseField(null, 'string', 'Wake-on-LAN', { enum: ['phy', 'unicast', 'broadcast', 'arp', 'magic', ''] }); this.Port = new BaseField(null, 'string', 'Port type', { enum: ['tp', 'aui', 'bnc', 'mii', 'fibre', ''] }); this.Advertise = new BaseField(null, 'strings', 'Advertised features'); this.RxFlowControl = new BooleanYesNo(); this.TxFlowControl = new BooleanYesNo(); } } /** * [Network] section configuration * @class NetworkSection */ class NetworkSection { constructor() { this.Description = new BaseField(null, 'string', 'Interface description'); this.DHCP = new BaseField(null, 'dhcp-mode', 'DHCP client', { enum: ['yes', 'no', 'ipv4', 'ipv6'] }); this.DHCPServer = new BooleanYesNo(); this.DNS = new BaseField(null, 'ip-addresses', 'DNS servers'); this.NTP = new BaseField(null, 'ip-addresses', 'NTP servers'); this.IPForward = new BaseField(null, 'ip-forward', 'IP forwarding', { enum: ['yes', 'no', 'ipv4', 'ipv6'] }); this.IPv6PrivacyExtensions = new BaseField(null, 'privacy-extensions', 'IPv6 privacy extensions', { enum: ['yes', 'no', 'prefer-public'] }); this.IPv6AcceptRA = new BooleanYesNo(); this.LLMNR = new BaseField(null, 'llmnr', 'LLMNR support', { enum: ['yes', 'no', 'resolve'] }); this.MulticastDNS = new BaseField(null, 'mdns', 'Multicast DNS', { enum: ['yes', 'no', 'resolve'] }); this.DNSSEC = new BaseField(null, 'dnssec', 'DNSSEC support', { enum: ['yes', 'no', 'allow-downgrade'] }); this.Domains = new BaseField(null, 'strings', 'DNS search domains'); this.ConfigureWithoutCarrier = new BooleanYesNo(); this.IgnoreCarrierLoss = new BooleanYesNo(); this.KeepConfiguration = new BaseField(null, 'number', 'Keep configuration time in seconds'); } } /** * [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, 'use-domains', 'Use domains from DHCP', { enum: ['yes', 'no', 'route'] }); this.ClientIdentifier = new BaseField(null, 'client-identifier', 'DHCP client identifier', { enum: ['mac', 'duid'] }); this.RouteMetric = new BaseField(null, 'number', 'Route metric for DHCP routes'); } } /** * [Address] section configuration * @class AddressSection */ class AddressSection { constructor() { this.Address = new BaseField(null, 'ip-prefix', 'IP address with prefix'); this.Peer = new BaseField(null, 'ip-address', 'Peer address'); this.Broadcast = new BaseField(null, 'ip-address', 'Broadcast address'); this.Label = new BaseField(null, 'string', 'Address label'); this.Scope = new BaseField(null, 'number', 'Address scope'); this.Flags = new BaseField(null, 'strings', 'Address flags'); } } /** * [Route] section configuration * @class RouteSection */ class RouteSection { constructor() { this.Gateway = new BaseField(null, 'ip-address', 'Gateway address'); this.GatewayOnLink = new BooleanYesNo(); this.Destination = new BaseField(null, 'ip-prefix', 'Destination prefix'); this.Source = new BaseField(null, 'ip-address', 'Source address'); this.PreferredSource = new BaseField(null, 'ip-address', 'Preferred source address'); this.Metric = new BaseField(null, 'number', 'Route metric'); this.Scope = new BaseField(null, 'route-scope', 'Route scope', { enum: ['global', 'link', 'host'] }); this.Type = new BaseField(null, 'route-type', 'Route type', { enum: ['unicast', 'local', 'broadcast', 'anycast', 'multicast', 'blackhole', 'unreachable', 'prohibit'] }); } } /** * Complete network configuration * @class NetworkConfiguration */ class NetworkConfiguration { constructor() { this.Match = new MatchSection(); this.Link = new LinkSection(); this.Network = new NetworkSection(); this.DHCP = new DHCPSection(); this.Address = []; 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), 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 * @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 '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; } } _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)); } // [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.join('\n') + '\n'; } _hasSectionValues(section) { return Object.values(section).some(field => field.value !== null && field.value !== undefined && field.value !== '' ); } _formatSection(section) { const lines = []; for (const [key, field] of Object.entries(section)) { if (field.value !== null && field.value !== undefined && field.value !== '') { lines.push(`${key}=${field.toString()}`); } } return lines; } } // Export classes export { BaseField, MACAddress, IPv4Address, IPv6Address, BooleanYesNo, Port, MTU, MatchSection, LinkSection, NetworkSection, DHCPSection, AddressSection, RouteSection, NetworkConfiguration };