diff options
38 files changed, 4562 insertions, 985 deletions
@@ -27,6 +27,7 @@ op_mode_definitions: rm -f $(OP_TMPL_DIR)/clear/node.def rm -f $(OP_TMPL_DIR)/set/node.def rm -f $(OP_TMPL_DIR)/show/node.def + rm -f $(OP_TMPL_DIR)/show/interfaces/node.def rm -f $(OP_TMPL_DIR)/reset/node.def rm -f $(OP_TMPL_DIR)/restart/node.def rm -f $(OP_TMPL_DIR)/monitor/node.def @@ -1,6 +1,7 @@ # vyos-1x: VyOS 1.2.0+ configuration scripts and data [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=vyos%3Avyos-1x&metric=coverage)](https://sonarcloud.io/component_measures?id=vyos%3Avyos-1x&metric=coverage) +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x?ref=badge_shield) VyOS 1.1.x had its codebase split into way too many submodules for no good reason, which made it hard to navigate or write meaningful changelogs. As the code undergoes rewrite in the new style in VyOS 1.2.0+, @@ -54,3 +55,7 @@ Tests are executed at build time, you can also execute them by hand with: ``` make test ``` + + +## License +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x?ref=badge_large)
\ No newline at end of file diff --git a/data/templates/rsyslog/rsyslog.conf b/data/templates/rsyslog/rsyslog.conf new file mode 100644 index 000000000..0910bd662 --- /dev/null +++ b/data/templates/rsyslog/rsyslog.conf @@ -0,0 +1,59 @@ +# /etc/rsyslog.conf Configuration file for rsyslog. +# + +################# +#### MODULES #### +################# + +$ModLoad imuxsock # provides support for local system logging +$ModLoad imklog # provides kernel logging support (previously done by rklogd) +#$ModLoad immark # provides --MARK-- message capability + +$OmitLocalLogging no +$SystemLogSocketName /run/systemd/journal/syslog + +$KLogPath /proc/kmsg + +# provides UDP syslog reception +#$ModLoad imudp +#$UDPServerRun 514 + +# provides TCP syslog reception +#$ModLoad imtcp +#$InputTCPServerRun 514 + +########################### +#### GLOBAL DIRECTIVES #### +########################### + +# +# Use traditional timestamp format. +# To enable high precision timestamps, comment out the following line. +# +$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat + +# Filter duplicated messages +$RepeatedMsgReduction on + +# +# Set the default permissions for all log files. +# +$FileOwner root +$FileGroup adm +$FileCreateMode 0640 +$DirCreateMode 0755 +$Umask 0022 + + +# +# Include all config files in /etc/rsyslog.d/ +# +$IncludeConfig /etc/rsyslog.d/*.conf + +############### +#### RULES #### +############### +# Emergencies are sent to everybody logged in. + +*.emerg :omusrmsg:* + diff --git a/debian/control b/debian/control index 70bf7a61c..0912acbcb 100644 --- a/debian/control +++ b/debian/control @@ -21,6 +21,8 @@ Depends: python3, python3-pystache, python3-psutil, python3-tabulate, + python3-six, + python3-isc-dhcp-leases, ipaddrcheck, tcpdump, bmon, @@ -36,8 +38,10 @@ Depends: python3, iputils-arping, libvyosconfig0, beep, + isc-dhcp-server, keepalived (>=2.0.5), wireguard, + tftpd-hpa, ${shlibs:Depends}, ${misc:Depends} Description: VyOS configuration scripts and data diff --git a/interface-definitions/bcast-relay.xml b/interface-definitions/bcast-relay.xml index 0437192fa..96ce16639 100644 --- a/interface-definitions/bcast-relay.xml +++ b/interface-definitions/bcast-relay.xml @@ -3,24 +3,36 @@ <interfaceDefinition> <node name="service"> <children> - <node name="broadcast-relay"> + <node name="broadcast-relay" owner="${vyos_conf_scripts_dir}/bcast_relay.py"> <properties> - <help>UDP Broadcast Relay parameters</help> + <help>UDP broadcast relay service</help> + <priority>990</priority> </properties> <children> - <tagNode name="id" owner="${vyos_conf_scripts_dir}/bcast_relay.py"> + <leafNode name="disable"> + <properties> + <help>Globally disable broadcast relay service</help> + <valueless/> + </properties> + </leafNode> + <tagNode name="id"> <properties> <help>Unique ID for each UDP port to forward</help> <valueHelp> <format>1-99</format> <description>Numerical ID #</description> </valueHelp> - <priority>990</priority> <constraint> <validator name="numeric" argument="--range 1-99"/> </constraint> </properties> <children> + <leafNode name="disable"> + <properties> + <help>Disable broadcast relay service instance</help> + <valueless/> + </properties> + </leafNode> <leafNode name="address"> <properties> <help>Set source IP of forwarded packets, otherwise original senders address is used</help> @@ -29,7 +41,7 @@ <description>Optional source address for forwarded packets</description> </valueHelp> <constraint> - <validator name="ipv4"/> + <validator name="ipv4-address"/> </constraint> </properties> </leafNode> diff --git a/interface-definitions/dhcp-server.xml b/interface-definitions/dhcp-server.xml new file mode 100644 index 000000000..2002f0c65 --- /dev/null +++ b/interface-definitions/dhcp-server.xml @@ -0,0 +1,456 @@ +<?xml version="1.0"?> +<!-- DHCP server configuration --> +<interfaceDefinition> + <node name="service"> + <children> + <node name="dhcp-server" owner="${vyos_conf_scripts_dir}/dhcp_server.py"> + <properties> + <help>Dynamic Host Configuration Protocol (DHCP) for DHCP server</help> + <priority>911</priority> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable DHCP server</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="dynamic-dns-update"> + <properties> + <help>DHCP server to dynamically update the Domain Name System (DNS)</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="global-parameters"> + <properties> + <help>Additional global parameters for DHCP server. You must + use the syntax of dhcpd.conf in this text-field. Using this + without proper knowledge may result in a crashed DHCP server. + Check system log to look for errors.</help> + <multi/> + </properties> + </leafNode> + <leafNode name="hostfile-update"> + <properties> + <help>Enable DHCP server updating /etc/hosts (per client lease)</help> + <valueless/> + </properties> + </leafNode> + <tagNode name="shared-network-name"> + <properties> + <help>DHCP shared network name [REQUIRED]</help> + <constraint> + <regex>^[-_a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Invalid DHCP pool name</constraintErrorMessage> + </properties> + <children> + <leafNode name="authoritative"> + <properties> + <help>Option to make DHCP server authoritative for this physical network</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="description"> + <properties> + <help>Shared-network-name description</help> + </properties> + </leafNode> + <leafNode name="disable"> + <properties> + <help>Option to disable DHCP configuration for shared-network</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="shared-network-parameters"> + <properties> + <help>Additional shared-network parameters for DHCP server. + You must use the syntax of dhcpd.conf in this text-field. + Using this without proper knowledge may result in a crashed + DHCP server. Check system log to look for errors.</help> + <multi/> + </properties> + </leafNode> + <tagNode name="subnet"> + <properties> + <help>DHCP subnet for shared network</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + </properties> + <children> + <leafNode name="bootfile-name"> + <properties> + <help>Bootstrap file name</help> + </properties> + </leafNode> + <leafNode name="bootfile-server"> + <properties> + <help>Server (IP address or domain name) from which the initial + boot file is to be loaded</help> + </properties> + </leafNode> + <leafNode name="client-prefix-length"> + <properties> + <help>Specifies the clients subnet mask as per RFC 950. If unset, subnet declaration is used.</help> + <valueHelp> + <format>0-32</format> + <description>DHCP client prefix length must be 0 to 32</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-32"/> + </constraint> + <constraintErrorMessage>DHCP client prefix length must be 0 to 32</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="default-router"> + <properties> + <help>IP address of default router</help> + <valueHelp> + <format>ipv4</format> + <description>Default router IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="dns-server"> + <properties> + <help>DNS server IPv4 address</help> + <valueHelp> + <format>ipv4</format> + <description>DNS server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="domain-name"> + <properties> + <help>Client domain name</help> + </properties> + </leafNode> + <leafNode name="domain-search"> + <properties> + <help>Client domain search</help> + <multi/> + </properties> + </leafNode> + <leafNode name="exclude"> + <properties> + <help>IP address that needs to be excluded from DHCP lease range</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address to exclude from lease range</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <node name="failover"> + <properties> + <help>DHCP failover parameters</help> + </properties> + <children> + <leafNode name="local-address"> + <properties> + <help>IP address for failover peer to connect [REQUIRED]</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address to exclude from lease range</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="name"> + <properties> + <help>DHCP failover peer name [REQUIRED]</help> + <constraint> + <regex>^[-_a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Invalid failover peer name</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="peer-address"> + <properties> + <help>IP address of failover peer [REQUIRED]</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address to exclude from lease range</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="status"> + <properties> + <help>DHCP failover peer status (primary|secondary) [REQUIRED]</help> + <completionHelp> + <list>primary secondary</list> + </completionHelp> + <constraint> + <regex>(primary|secondary)</regex> + </constraint> + <constraintErrorMessage>Invalid DHCP failover peer status</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <leafNode name="ip-forwarding"> + <properties> + <help>Enable IP forwarding on client</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="lease"> + <properties> + <help>Lease timeout in seconds (default: 86400)</help> + <valueHelp> + <format>0-4294967295</format> + <description>DHCP lease time in seconds must be between 0 and 4294967295 (49 days)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + <constraintErrorMessage>DHCP lease time must be 0 to 4294967295</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="ntp-server"> + <properties> + <help>IP address of NTP server</help> + <valueHelp> + <format>ipv4</format> + <description>NTP server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="pop-server"> + <properties> + <help>IP address of POP3 server</help> + <valueHelp> + <format>ipv4</format> + <description>POP3 server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="server-identifier"> + <properties> + <help>Address for DHCP server identifier</help> + <valueHelp> + <format>ipv4</format> + <description>DHCP server identifier IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="smtp-server"> + <properties> + <help>IP address of SMTP server</help> + <valueHelp> + <format>ipv4</format> + <description>SMTP server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <tagNode name="range"> + <properties> + <help>DHCP lease range</help> + <constraint> + <regex>^[-_a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Invalid DHCP lease range name</constraintErrorMessage> + </properties> + <children> + <leafNode name="start"> + <properties> + <help>First IP address for DHCP lease range</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 start address of pool</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="stop"> + <properties> + <help>Last IP address for DHCP lease range</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 end address of pool</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <tagNode name="static-mapping"> + <properties> + <help>Static mapping for specified address type</help> + <constraint> + <regex>^[-_a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Invalid static-mapping name</constraintErrorMessage> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable static-mapping</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ip-address"> + <properties> + <help>Static mapping for specified IP address [REQUIRED]</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address used in static mapping</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="mac-address"> + <properties> + <help>Static mapping for specified MAC address [REQUIRED]</help> + <valueHelp> + <format>h:h:h:h:h:h</format> + <description>MAC address used in static mapping [REQUIRED]</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="static-mapping-parameters"> + <properties> + <help>Additional static-mapping parameters for DHCP server. + You must use the syntax of dhcpd.conf in this text-field. + Using this without proper knowledge may result in a crashed + DHCP server. Check system log to look for errors.</help> + <multi/> + </properties> + </leafNode> + </children> + </tagNode> + <node name="static-route"> + <properties> + <help>Classless static route</help> + </properties> + <children> + <leafNode name="destination-subnet"> + <properties> + <help>Destination subnet [REQUIRED]</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + </properties> + </leafNode> + <leafNode name="router"> + <properties> + <help>IP address of router to be used to reach the destination subnet [REQUIRED]</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address of router</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="subnet-parameters"> + <properties> + <help>Additional subnet parameters for DHCP server. You must + use the syntax of dhcpd.conf in this text-field. Using this + without proper knowledge may result in a crashed DHCP server. + Check system log to look for errors.</help> + <multi/> + </properties> + </leafNode> + <leafNode name="tftp-server-name"> + <properties> + <help>TFTP server name</help> + </properties> + </leafNode> + <leafNode name="time-offset"> + <properties> + <help>Offset of the client's subnet in seconds from Coordinated Universal Time (UTC)</help> + <constraint> + <regex>^-?[0-9]+$</regex> + </constraint> + <constraintErrorMessage>Invalid time offset valuee</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="time-server"> + <properties> + <help>IP address of time server</help> + <valueHelp> + <format>ipv4</format> + <description>Time server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="wins-server"> + <properties> + <help>IP address for Windows Internet Name Service (WINS) server</help> + <valueHelp> + <format>ipv4</format> + <description>WINS server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="wpad-url"> + <properties> + <help>Web Proxy Autodiscovery (WPAD) URL</help> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/dhcpv6-server.xml b/interface-definitions/dhcpv6-server.xml new file mode 100644 index 000000000..e63eb2242 --- /dev/null +++ b/interface-definitions/dhcpv6-server.xml @@ -0,0 +1,316 @@ +<?xml version="1.0"?> +<!-- DHCPv6 server configuration --> +<interfaceDefinition> + <node name="service"> + <children> + <node name="dhcpv6-server" owner="${vyos_conf_scripts_dir}/dhcpv6_server.py"> + <properties> + <help>DHCP for IPv6 (DHCPv6) server</help> + <priority>900</priority> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable DHCPv6 server</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="preference"> + <properties> + <help>Preference of this DHCPv6 server compared with others</help> + <valueHelp> + <format>0-255</format> + <description>DHCPv6 server preference (0-255)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + <constraintErrorMessage>Preference must be between 0 and 255</constraintErrorMessage> + </properties> + </leafNode> + <tagNode name="shared-network-name"> + <properties> + <help>DHCPv6 shared network name [REQUIRED]</help> + <constraint> + <regex>^[-_a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Invalid DHCPv6 pool name</constraintErrorMessage> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable DHCPv6 configuration for shared-network</help> + <valueless/> + </properties> + </leafNode> + <tagNode name="subnet"> + <properties> + <help>IPv6 DHCP subnet for this shared network [REQUIRED]</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + <children> + <node name="address-range"> + <properties> + <help>Parameters setting ranges for assigning IPv6 addresses</help> + </properties> + <children> + <tagNode name="prefix"> + <properties> + <help>IPv6 prefix defining range of addresses to assign</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + <children> + <leafNode name="temporary"> + <properties> + <help>Address range will be used for temporary addresses</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + <tagNode name="start"> + <properties> + <help>First in range of consecutive IPv6 addresses to assign</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + <children> + <leafNode name="stop"> + <properties> + <help>Last in range of consecutive IPv6 addresses</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + <leafNode name="domain-search"> + <properties> + <help>Domain name for client to search</help> + <constraint> + <regex>^[-_a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Invalid domain name syntax</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + <node name="lease-time"> + <properties> + <help>Parameters relating to the lease time</help> + </properties> + <children> + <leafNode name="default"> + <properties> + <help>Default time (in seconds) that will be assigned to a lease</help> + </properties> + </leafNode> + <leafNode name="maximum"> + <properties> + <help>Maximum time (in seconds) that will be assigned to a lease</help> + </properties> + </leafNode> + <leafNode name="minimum"> + <properties> + <help>Minimum time (in seconds) that will be assigned to a lease</help> + </properties> + </leafNode> + </children> + </node> + <leafNode name="name-server"> + <properties> + <help>IPv6 address of a Recursive DNS Server</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of DNS name server</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="nis-domain"> + <properties> + <help>NIS domain name for client to use</help> + <constraint> + <regex>^[-_a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Invalid NIS domain name syntax</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="nis-server"> + <properties> + <help>IPv6 address of a NIS Server</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of NIS server</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="nisplus-domain"> + <properties> + <help>NIS+ domain name for client to use</help> + <constraint> + <regex>^[-_a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Invalid NIS+ domain name syntax</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="nisplus-server"> + <properties> + <help>IPv6 address of a NIS+ Server</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of NIS+ server</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <node name="prefix-delegation"> + <properties> + <help>Parameters relating to IPv6 prefix delegation</help> + </properties> + <children> + <tagNode name="start"> + <properties> + <help>First in range of IPv6 addresses to be used in prefix delegation</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address used in prefix delegation</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + <children> + <leafNode name="prefix-length"> + <properties> + <help>Length in bits of prefixes to be delegated</help> + <valueHelp> + <format>0-255</format> + <description>DHCPv6 server preference (0-255)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + <constraintErrorMessage>Preference must be between 0 and 255</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="stop"> + <properties> + <help>Last in range of IPv6 addresses to be used in prefix delegation</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address used in prefix delegation</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + <leafNode name="sip-server-address"> + <properties> + <help>IPv6 address of SIP server</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of SIP server</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="sip-server-name"> + <properties> + <help>SIP server name</help> + <constraint> + <regex>^[-_a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Invalid SIP server name syntax</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + <leafNode name="sntp-server"> + <properties> + <help>IPv6 address of an SNTP Server for client to use</help> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <tagNode name="static-mapping"> + <properties> + <help>Name of static mapping</help> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable static-mapping</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="identifier"> + <properties> + <help>Client identifier for this static mapping</help> + </properties> + </leafNode> + <leafNode name="ipv6-address"> + <properties> + <help>Client IPv5 address for this static mapping</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address for this tatic mapping</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/mdns-repeater.xml b/interface-definitions/mdns-repeater.xml index d74e203d6..a59321294 100644 --- a/interface-definitions/mdns-repeater.xml +++ b/interface-definitions/mdns-repeater.xml @@ -14,9 +14,15 @@ <priority>990</priority> </properties> <children> + <leafNode name="disable"> + <properties> + <help>Disable mDNS repeater service</help> + <valueless/> + </properties> + </leafNode> <leafNode name="interface"> <properties> - <help>Interface to repeat mdns advertisements to [REQUIRED]</help> + <help>Interface to repeat mDNS advertisements [REQUIRED]</help> <completionHelp> <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> diff --git a/interface-definitions/syslog.xml b/interface-definitions/syslog.xml index 0776fff56..aafa91b55 100644 --- a/interface-definitions/syslog.xml +++ b/interface-definitions/syslog.xml @@ -1,5 +1,4 @@ <?xml version="1.0"?> - <interfaceDefinition> <node name="system"> <children> @@ -9,660 +8,662 @@ <priority>400</priority> </properties> <children> - <tagNode name="user"> - <properties> - <help>Logging to specific user's terminal</help> - <constraint> - <regex>^[a-z_][a-z0-9_-]{1,31}[$]?</regex> - </constraint> - <constraintErrorMessage>illegal characters in user</constraintErrorMessage> - <valueHelp> - <format>username</format> - <description>user login name</description> - </valueHelp> - </properties> - <children> - <tagNode name="facility"> - <properties> - <help>Facility for logging</help> - <completionHelp> - <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> - </completionHelp> - <valueHelp> - <format>all</format> - <description>All facilities excluding "mark"</description> - </valueHelp> - <valueHelp> - <format>auth</format> - <description>Authentication and authorization</description> - </valueHelp> - <valueHelp> - <format>authpriv</format> - <description>Non-system authorization</description> - </valueHelp> - <valueHelp> - <format>cron</format> - <description>Cron daemon</description> - </valueHelp> - <valueHelp> - <format>daemon</format> - <description>System daemons</description> - </valueHelp> - <valueHelp> - <format>kern</format> - <description>Kernel</description> - </valueHelp> - <valueHelp> - <format>lpr</format> - <description>Line printer spooler</description> - </valueHelp> - <valueHelp> - <format>mail</format> - <description>Mail subsystem</description> - </valueHelp> - <valueHelp> - <format>mark</format> - <description>Timestamp</description> - </valueHelp> - <valueHelp> - <format>news</format> - <description>USENET subsystem</description> - </valueHelp> - <valueHelp> - <format>protocols</format> - <description>depricated will be set to local7</description> - </valueHelp> - <valueHelp> - <format>security</format> - <description>depricated will be set to auth</description> - </valueHelp> - <valueHelp> - <format>syslog</format> - <description>Authentication and authorization</description> - </valueHelp> - <valueHelp> - <format>user</format> - <description>Application processes</description> - </valueHelp> - <valueHelp> - <format>uucp</format> - <description>UUCP subsystem</description> - </valueHelp> - <valueHelp> - <format>local0</format> - <description>Local facility 0</description> - </valueHelp> - <valueHelp> - <format>local1</format> - <description>Local facility 1</description> - </valueHelp> - <valueHelp> - <format>local2</format> - <description>Local facility 2</description> - </valueHelp> - <valueHelp> - <format>local3</format> - <description>Local facility 3</description> - </valueHelp> - <valueHelp> - <format>local4</format> - <description>Local facility 4</description> - </valueHelp> - <valueHelp> - <format>local5</format> - <description>Local facility 5</description> - </valueHelp> - <valueHelp> - <format>local6</format> - <description>Local facility 6</description> - </valueHelp> - <valueHelp> - <format>local7</format> - <description>Local facility 7</description> - </valueHelp> - </properties> - <children> - <leafNode name="level"> - <properties> - <help>Logging level</help> - <completionHelp> - <list>emerg alert crit err warning notice info debug all</list> - </completionHelp> - <valueHelp> - <format>emerg</format> - <description>Emergency messages</description> - </valueHelp> - <valueHelp> - <format>alert</format> - <description>Urgent messages</description> - </valueHelp> - <valueHelp> - <format>crit</format> - <description>Critical messages</description> - </valueHelp> - <valueHelp> - <format>err</format> - <description>Error messages</description> - </valueHelp> - <valueHelp> - <format>warning</format> - <description>Warning messages</description> - </valueHelp> - <valueHelp> - <format>notice</format> - <description>Messages for further investigation</description> - </valueHelp> - <valueHelp> - <format>info</format> - <description>Informational messages</description> - </valueHelp> - <valueHelp> - <format>debug</format> - <description>Debug messages</description> - </valueHelp> - <valueHelp> - <format>all</format> - <description>Log everything</description> - </valueHelp> - </properties> - </leafNode> - </children> - </tagNode> - </children> - </tagNode> - <tagNode name="host"> - <properties> - <help>Logging to a remote host</help> - <constraint> - <!-- at least let's make sure whitespace isn't allowed, ideally it should be checked for IPv4/IPv6 address or fqdn/hostname --> - <regex>[^ ]{1,63}</regex> - </constraint> - <constraintErrorMessage>illegal characters in user</constraintErrorMessage> - <valueHelp> - <format>x.x.x.x or host.domain.tld</format> - <description>Remote host name or IP address</description> - </valueHelp> - </properties> - <children> - <tagNode name="facility"> - <properties> - <help>Facility for logging</help> - <completionHelp> - <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> - </completionHelp> - <valueHelp> - <format>all</format> - <description>All facilities excluding "mark"</description> - </valueHelp> - <valueHelp> - <format>auth</format> - <description>Authentication and authorization</description> - </valueHelp> - <valueHelp> - <format>authpriv</format> - <description>Non-system authorization</description> - </valueHelp> - <valueHelp> - <format>cron</format> - <description>Cron daemon</description> - </valueHelp> - <valueHelp> - <format>daemon</format> - <description>System daemons</description> - </valueHelp> - <valueHelp> - <format>kern</format> - <description>Kernel</description> - </valueHelp> - <valueHelp> - <format>lpr</format> - <description>Line printer spooler</description> - </valueHelp> - <valueHelp> - <format>mail</format> - <description>Mail subsystem</description> - </valueHelp> - <valueHelp> - <format>mark</format> - <description>Timestamp</description> - </valueHelp> - <valueHelp> - <format>news</format> - <description>USENET subsystem</description> - </valueHelp> - <valueHelp> - <format>protocols</format> - <description>depricated will be set to local7</description> - </valueHelp> - <valueHelp> - <format>security</format> - <description>depricated will be set to auth</description> - </valueHelp> - <valueHelp> - <format>syslog</format> - <description>Authentication and authorization</description> - </valueHelp> - <valueHelp> - <format>user</format> - <description>Application processes</description> - </valueHelp> - <valueHelp> - <format>uucp</format> - <description>UUCP subsystem</description> - </valueHelp> - <valueHelp> - <format>local0</format> - <description>Local facility 0</description> - </valueHelp> - <valueHelp> - <format>local1</format> - <description>Local facility 1</description> - </valueHelp> - <valueHelp> - <format>local2</format> - <description>Local facility 2</description> - </valueHelp> - <valueHelp> - <format>local3</format> - <description>Local facility 3</description> - </valueHelp> - <valueHelp> - <format>local4</format> - <description>Local facility 4</description> - </valueHelp> - <valueHelp> - <format>local5</format> - <description>Local facility 5</description> - </valueHelp> - <valueHelp> - <format>local6</format> - <description>Local facility 6</description> - </valueHelp> - <valueHelp> - <format>local7</format> - <description>Local facility 7</description> - </valueHelp> - </properties> - <children> - <leafNode name="protocol"> - <properties> - <help>syslog communication protocol</help> - <valueHelp> - <format>udp</format> - <description>send log messages to remote syslog server over udp</description> - </valueHelp> - <valueHelp> - <format>tcp</format> - <description>send log messages to remote syslog server over tdp</description> - </valueHelp> - </properties> - </leafNode> - - <leafNode name="level"> - <properties> - <help>Logging level</help> - <completionHelp> - <list>emerg alert crit err warning notice info debug all</list> - </completionHelp> - <valueHelp> - <format>emerg</format> - <description>Emergency messages</description> - </valueHelp> - <valueHelp> - <format>alert</format> - <description>Urgent messages</description> - </valueHelp> - <valueHelp> - <format>crit</format> - <description>Critical messages</description> - </valueHelp> - <valueHelp> - <format>err</format> - <description>Error messages</description> - </valueHelp> - <valueHelp> - <format>warning</format> - <description>Warning messages</description> - </valueHelp> - <valueHelp> - <format>notice</format> - <description>Messages for further investigation</description> - </valueHelp> - <valueHelp> - <format>info</format> - <description>Informational messages</description> - </valueHelp> - <valueHelp> - <format>debug</format> - <description>Debug messages</description> - </valueHelp> - <valueHelp> - <format>all</format> - <description>Log everything</description> - </valueHelp> - </properties> - </leafNode> - </children> - </tagNode> - </children> - </tagNode> - <node name="global"> - <children> - <node name="archive"> - <properties> - <help>Log file size and rotation characteristics</help> - </properties> - <children> - <leafNode name="file"> - <properties> - <help>Number of saved files (default is 5)</help> - <constraint> - <regex>^[0-9]+</regex> - </constraint> - <constraintErrorMessage>illegal characters in number of files</constraintErrorMessage> - </properties> - </leafNode> - <leafNode name="size"> - <properties> - <help>Size of log files (in kbytes, default is 256)</help> - <constraint> - <regex>^[0-9]+</regex> - </constraint> - <constraintErrorMessage>illegal characters in size</constraintErrorMessage> - </properties> - </leafNode> - </children> - </node> - <tagNode name="facility"> - <properties> - <help>Facility for logging</help> - <completionHelp> - <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> - </completionHelp> - <valueHelp> - <format>all</format> - <description>All facilities excluding "mark"</description> - </valueHelp> - <valueHelp> - <format>auth</format> - <description>Authentication and authorization</description> - </valueHelp> - <valueHelp> - <format>authpriv</format> - <description>Non-system authorization</description> - </valueHelp> - <valueHelp> - <format>cron</format> - <description>Cron daemon</description> - </valueHelp> - <valueHelp> - <format>daemon</format> - <description>System daemons</description> - </valueHelp> - <valueHelp> - <format>kern</format> - <description>Kernel</description> - </valueHelp> - <valueHelp> - <format>lpr</format> - <description>Line printer spooler</description> - </valueHelp> - <valueHelp> - <format>mail</format> - <description>Mail subsystem</description> - </valueHelp> - <valueHelp> - <format>mark</format> - <description>Timestamp</description> - </valueHelp> - <valueHelp> - <format>news</format> - <description>USENET subsystem</description> - </valueHelp> - <valueHelp> - <format>protocols</format> - <description>depricated will be set to local7</description> - </valueHelp> - <valueHelp> - <format>security</format> - <description>depricated will be set to auth</description> - </valueHelp> - <valueHelp> - <format>syslog</format> - <description>Authentication and authorization</description> - </valueHelp> - <valueHelp> - <format>user</format> - <description>Application processes</description> - </valueHelp> - <valueHelp> - <format>uucp</format> - <description>UUCP subsystem</description> - </valueHelp> - <valueHelp> - <format>local0</format> - <description>Local facility 0</description> - </valueHelp> - <valueHelp> - <format>local1</format> - <description>Local facility 1</description> - </valueHelp> - <valueHelp> - <format>local2</format> - <description>Local facility 2</description> - </valueHelp> - <valueHelp> - <format>local3</format> - <description>Local facility 3</description> - </valueHelp> - <valueHelp> - <format>local4</format> - <description>Local facility 4</description> - </valueHelp> - <valueHelp> - <format>local5</format> - <description>Local facility 5</description> - </valueHelp> - <valueHelp> - <format>local6</format> - <description>Local facility 6</description> - </valueHelp> - <valueHelp> - <format>local7</format> - <description>Local facility 7</description> - </valueHelp> - </properties> - <children> - <leafNode name="level"> - <properties> - <help>Logging level</help> - <completionHelp> - <list>emerg alert crit err warning notice info debug all</list> - </completionHelp> - <valueHelp> - <format>emerg</format> - <description>Emergency messages</description> - </valueHelp> - <valueHelp> - <format>alert</format> - <description>Urgent messages</description> - </valueHelp> - <valueHelp> - <format>crit</format> - <description>Critical messages</description> - </valueHelp> - <valueHelp> - <format>err</format> - <description>Error messages</description> - </valueHelp> - <valueHelp> - <format>warning</format> - <description>Warning messages</description> - </valueHelp> - <valueHelp> - <format>notice</format> - <description>Messages for further investigation</description> - </valueHelp> - <valueHelp> - <format>info</format> - <description>Informational messages</description> - </valueHelp> - <valueHelp> - <format>debug</format> - <description>Debug messages</description> - </valueHelp> - <valueHelp> - <format>all</format> - <description>Log everything</description> - </valueHelp> - </properties> - </leafNode> - </children> - </tagNode> - </children> - </node> - <tagNode name="file"> - <properties> - <help>Logging to a file</help> - <constraint> - <regex>^[a-zA-Z0-9\-_.]{1,255}</regex> + <tagNode name="user"> + <properties> + <help>Logging to specific user's terminal</help> + <constraint> + <regex>^[a-z_][a-z0-9_-]{1,31}[$]?</regex> + </constraint> + <constraintErrorMessage>illegal characters in user</constraintErrorMessage> + <valueHelp> + <format>username</format> + <description>user login name</description> + </valueHelp> + </properties> + <children> + <tagNode name="facility"> + <properties> + <help>Facility for logging</help> + <completionHelp> + <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> + </completionHelp> + <valueHelp> + <format>all</format> + <description>All facilities excluding "mark"</description> + </valueHelp> + <valueHelp> + <format>auth</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>authpriv</format> + <description>Non-system authorization</description> + </valueHelp> + <valueHelp> + <format>cron</format> + <description>Cron daemon</description> + </valueHelp> + <valueHelp> + <format>daemon</format> + <description>System daemons</description> + </valueHelp> + <valueHelp> + <format>kern</format> + <description>Kernel</description> + </valueHelp> + <valueHelp> + <format>lpr</format> + <description>Line printer spooler</description> + </valueHelp> + <valueHelp> + <format>mail</format> + <description>Mail subsystem</description> + </valueHelp> + <valueHelp> + <format>mark</format> + <description>Timestamp</description> + </valueHelp> + <valueHelp> + <format>news</format> + <description>USENET subsystem</description> + </valueHelp> + <valueHelp> + <format>protocols</format> + <description>depricated will be set to local7</description> + </valueHelp> + <valueHelp> + <format>security</format> + <description>depricated will be set to auth</description> + </valueHelp> + <valueHelp> + <format>syslog</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>user</format> + <description>Application processes</description> + </valueHelp> + <valueHelp> + <format>uucp</format> + <description>UUCP subsystem</description> + </valueHelp> + <valueHelp> + <format>local0</format> + <description>Local facility 0</description> + </valueHelp> + <valueHelp> + <format>local1</format> + <description>Local facility 1</description> + </valueHelp> + <valueHelp> + <format>local2</format> + <description>Local facility 2</description> + </valueHelp> + <valueHelp> + <format>local3</format> + <description>Local facility 3</description> + </valueHelp> + <valueHelp> + <format>local4</format> + <description>Local facility 4</description> + </valueHelp> + <valueHelp> + <format>local5</format> + <description>Local facility 5</description> + </valueHelp> + <valueHelp> + <format>local6</format> + <description>Local facility 6</description> + </valueHelp> + <valueHelp> + <format>local7</format> + <description>Local facility 7</description> + </valueHelp> + </properties> + <children> + <leafNode name="level"> + <properties> + <help>Logging level</help> + <completionHelp> + <list>emerg alert crit err warning notice info debug all</list> + </completionHelp> + <valueHelp> + <format>emerg</format> + <description>Emergency messages</description> + </valueHelp> + <valueHelp> + <format>alert</format> + <description>Urgent messages</description> + </valueHelp> + <valueHelp> + <format>crit</format> + <description>Critical messages</description> + </valueHelp> + <valueHelp> + <format>err</format> + <description>Error messages</description> + </valueHelp> + <valueHelp> + <format>warning</format> + <description>Warning messages</description> + </valueHelp> + <valueHelp> + <format>notice</format> + <description>Messages for further investigation</description> + </valueHelp> + <valueHelp> + <format>info</format> + <description>Informational messages</description> + </valueHelp> + <valueHelp> + <format>debug</format> + <description>Debug messages</description> + </valueHelp> + <valueHelp> + <format>all</format> + <description>Log everything</description> + </valueHelp> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + <tagNode name="host"> + <properties> + <help>Logging to a remote host</help> + <constraint> + <!-- at least let's make sure whitespace isn't allowed, ideally it should be checked for IPv4/IPv6 address or fqdn/hostname --> + <regex>[^ ]{1,63}</regex> + </constraint> + <constraintErrorMessage>illegal characters in user</constraintErrorMessage> + <valueHelp> + <format>x.x.x.x or host.domain.tld</format> + <description>Remote host name or IP address</description> + </valueHelp> + </properties> + <children> + <tagNode name="facility"> + <properties> + <help>Facility for logging</help> + <completionHelp> + <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> + </completionHelp> + <valueHelp> + <format>all</format> + <description>All facilities excluding "mark"</description> + </valueHelp> + <valueHelp> + <format>auth</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>authpriv</format> + <description>Non-system authorization</description> + </valueHelp> + <valueHelp> + <format>cron</format> + <description>Cron daemon</description> + </valueHelp> + <valueHelp> + <format>daemon</format> + <description>System daemons</description> + </valueHelp> + <valueHelp> + <format>kern</format> + <description>Kernel</description> + </valueHelp> + <valueHelp> + <format>lpr</format> + <description>Line printer spooler</description> + </valueHelp> + <valueHelp> + <format>mail</format> + <description>Mail subsystem</description> + </valueHelp> + <valueHelp> + <format>mark</format> + <description>Timestamp</description> + </valueHelp> + <valueHelp> + <format>news</format> + <description>USENET subsystem</description> + </valueHelp> + <valueHelp> + <format>protocols</format> + <description>depricated will be set to local7</description> + </valueHelp> + <valueHelp> + <format>security</format> + <description>depricated will be set to auth</description> + </valueHelp> + <valueHelp> + <format>syslog</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>user</format> + <description>Application processes</description> + </valueHelp> + <valueHelp> + <format>uucp</format> + <description>UUCP subsystem</description> + </valueHelp> + <valueHelp> + <format>local0</format> + <description>Local facility 0</description> + </valueHelp> + <valueHelp> + <format>local1</format> + <description>Local facility 1</description> + </valueHelp> + <valueHelp> + <format>local2</format> + <description>Local facility 2</description> + </valueHelp> + <valueHelp> + <format>local3</format> + <description>Local facility 3</description> + </valueHelp> + <valueHelp> + <format>local4</format> + <description>Local facility 4</description> + </valueHelp> + <valueHelp> + <format>local5</format> + <description>Local facility 5</description> + </valueHelp> + <valueHelp> + <format>local6</format> + <description>Local facility 6</description> + </valueHelp> + <valueHelp> + <format>local7</format> + <description>Local facility 7</description> + </valueHelp> + </properties> + <children> + <leafNode name="protocol"> + <properties> + <help>syslog communication protocol</help> + <valueHelp> + <format>udp</format> + <description>send log messages to remote syslog server over udp</description> + </valueHelp> + <valueHelp> + <format>tcp</format> + <description>send log messages to remote syslog server over tdp</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="level"> + <properties> + <help>Logging level</help> + <completionHelp> + <list>emerg alert crit err warning notice info debug all</list> + </completionHelp> + <valueHelp> + <format>emerg</format> + <description>Emergency messages</description> + </valueHelp> + <valueHelp> + <format>alert</format> + <description>Urgent messages</description> + </valueHelp> + <valueHelp> + <format>crit</format> + <description>Critical messages</description> + </valueHelp> + <valueHelp> + <format>err</format> + <description>Error messages</description> + </valueHelp> + <valueHelp> + <format>warning</format> + <description>Warning messages</description> + </valueHelp> + <valueHelp> + <format>notice</format> + <description>Messages for further investigation</description> + </valueHelp> + <valueHelp> + <format>info</format> + <description>Informational messages</description> + </valueHelp> + <valueHelp> + <format>debug</format> + <description>Debug messages</description> + </valueHelp> + <valueHelp> + <format>all</format> + <description>Log everything</description> + </valueHelp> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + <node name="global"> + <properties> + <help>Logging to system standard location</help> + </properties> + <children> + <node name="archive"> + <properties> + <help>Log file size and rotation characteristics</help> + </properties> + <children> + <leafNode name="file"> + <properties> + <help>Number of saved files (default is 5)</help> + <constraint> + <regex>^[0-9]+</regex> + </constraint> + <constraintErrorMessage>illegal characters in number of files</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="size"> + <properties> + <help>Size of log files (in kbytes, default is 256)</help> + <constraint> + <regex>^[0-9]+</regex> + </constraint> + <constraintErrorMessage>illegal characters in size</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <tagNode name="facility"> + <properties> + <help>Facility for logging</help> + <completionHelp> + <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> + </completionHelp> + <valueHelp> + <format>all</format> + <description>All facilities excluding "mark"</description> + </valueHelp> + <valueHelp> + <format>auth</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>authpriv</format> + <description>Non-system authorization</description> + </valueHelp> + <valueHelp> + <format>cron</format> + <description>Cron daemon</description> + </valueHelp> + <valueHelp> + <format>daemon</format> + <description>System daemons</description> + </valueHelp> + <valueHelp> + <format>kern</format> + <description>Kernel</description> + </valueHelp> + <valueHelp> + <format>lpr</format> + <description>Line printer spooler</description> + </valueHelp> + <valueHelp> + <format>mail</format> + <description>Mail subsystem</description> + </valueHelp> + <valueHelp> + <format>mark</format> + <description>Timestamp</description> + </valueHelp> + <valueHelp> + <format>news</format> + <description>USENET subsystem</description> + </valueHelp> + <valueHelp> + <format>protocols</format> + <description>depricated will be set to local7</description> + </valueHelp> + <valueHelp> + <format>security</format> + <description>depricated will be set to auth</description> + </valueHelp> + <valueHelp> + <format>syslog</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>user</format> + <description>Application processes</description> + </valueHelp> + <valueHelp> + <format>uucp</format> + <description>UUCP subsystem</description> + </valueHelp> + <valueHelp> + <format>local0</format> + <description>Local facility 0</description> + </valueHelp> + <valueHelp> + <format>local1</format> + <description>Local facility 1</description> + </valueHelp> + <valueHelp> + <format>local2</format> + <description>Local facility 2</description> + </valueHelp> + <valueHelp> + <format>local3</format> + <description>Local facility 3</description> + </valueHelp> + <valueHelp> + <format>local4</format> + <description>Local facility 4</description> + </valueHelp> + <valueHelp> + <format>local5</format> + <description>Local facility 5</description> + </valueHelp> + <valueHelp> + <format>local6</format> + <description>Local facility 6</description> + </valueHelp> + <valueHelp> + <format>local7</format> + <description>Local facility 7</description> + </valueHelp> + </properties> + <children> + <leafNode name="level"> + <properties> + <help>Logging level</help> + <completionHelp> + <list>emerg alert crit err warning notice info debug all</list> + </completionHelp> + <valueHelp> + <format>emerg</format> + <description>Emergency messages</description> + </valueHelp> + <valueHelp> + <format>alert</format> + <description>Urgent messages</description> + </valueHelp> + <valueHelp> + <format>crit</format> + <description>Critical messages</description> + </valueHelp> + <valueHelp> + <format>err</format> + <description>Error messages</description> + </valueHelp> + <valueHelp> + <format>warning</format> + <description>Warning messages</description> + </valueHelp> + <valueHelp> + <format>notice</format> + <description>Messages for further investigation</description> + </valueHelp> + <valueHelp> + <format>info</format> + <description>Informational messages</description> + </valueHelp> + <valueHelp> + <format>debug</format> + <description>Debug messages</description> + </valueHelp> + <valueHelp> + <format>all</format> + <description>Log everything</description> + </valueHelp> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + <tagNode name="file"> + <properties> + <help>Logging to a file</help> + <constraint> + <regex>^[a-zA-Z0-9\-_.]{1,255}</regex> </constraint> <constraintErrorMessage>illegal characters in filename or filename longer than 255 characters</constraintErrorMessage> - </properties> - <children> - <node name="archive"> - <properties> - <help>Log file size and rotation characteristics</help> - </properties> - <children> - <leafNode name="file"> - <properties> - <help>Number of saved files (default is 5)</help> - <constraint> - <regex>^[0-9]+</regex> - </constraint> - <constraintErrorMessage>illegal characters in number of files</constraintErrorMessage> - </properties> - </leafNode> - <leafNode name="size"> - <properties> - <help>Size of log files (in kbytes, default is 256)</help> - <constraint> - <regex>^[0-9]+</regex> - </constraint> - <constraintErrorMessage>illegal characters in size</constraintErrorMessage> - </properties> - </leafNode> - </children> - </node> - <tagNode name="facility"> - <properties> - <help>Facility for logging</help> - <completionHelp> - <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> - </completionHelp> - <valueHelp> - <format>all</format> - <description>All facilities excluding "mark"</description> - </valueHelp> - <valueHelp> - <format>auth</format> - <description>Authentication and authorization</description> - </valueHelp> - <valueHelp> - <format>authpriv</format> - <description>Non-system authorization</description> - </valueHelp> - <valueHelp> - <format>cron</format> - <description>Cron daemon</description> - </valueHelp> - <valueHelp> - <format>daemon</format> - <description>System daemons</description> - </valueHelp> - <valueHelp> - <format>kern</format> - <description>Kernel</description> - </valueHelp> - <valueHelp> - <format>lpr</format> - <description>Line printer spooler</description> - </valueHelp> - <valueHelp> - <format>mail</format> - <description>Mail subsystem</description> - </valueHelp> - <valueHelp> - <format>mark</format> - <description>Timestamp</description> - </valueHelp> - <valueHelp> - <format>news</format> - <description>USENET subsystem</description> - </valueHelp> - <valueHelp> - <format>protocols</format> - <description>depricated will be set to local7</description> - </valueHelp> - <valueHelp> - <format>security</format> - <description>depricated will be set to auth</description> - </valueHelp> - <valueHelp> - <format>syslog</format> + </properties> + <children> + <node name="archive"> + <properties> + <help>Log file size and rotation characteristics</help> + </properties> + <children> + <leafNode name="file"> + <properties> + <help>Number of saved files (default is 5)</help> + <constraint> + <regex>^[0-9]+</regex> + </constraint> + <constraintErrorMessage>illegal characters in number of files</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="size"> + <properties> + <help>Size of log files (in kbytes, default is 256)</help> + <constraint> + <regex>^[0-9]+</regex> + </constraint> + <constraintErrorMessage>illegal characters in size</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <tagNode name="facility"> + <properties> + <help>Facility for logging</help> + <completionHelp> + <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> + </completionHelp> + <valueHelp> + <format>all</format> + <description>All facilities excluding "mark"</description> + </valueHelp> + <valueHelp> + <format>auth</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>authpriv</format> + <description>Non-system authorization</description> + </valueHelp> + <valueHelp> + <format>cron</format> + <description>Cron daemon</description> + </valueHelp> + <valueHelp> + <format>daemon</format> + <description>System daemons</description> + </valueHelp> + <valueHelp> + <format>kern</format> + <description>Kernel</description> + </valueHelp> + <valueHelp> + <format>lpr</format> + <description>Line printer spooler</description> + </valueHelp> + <valueHelp> + <format>mail</format> + <description>Mail subsystem</description> + </valueHelp> + <valueHelp> + <format>mark</format> + <description>Timestamp</description> + </valueHelp> + <valueHelp> + <format>news</format> + <description>USENET subsystem</description> + </valueHelp> + <valueHelp> + <format>protocols</format> + <description>depricated will be set to local7</description> + </valueHelp> + <valueHelp> + <format>security</format> + <description>depricated will be set to auth</description> + </valueHelp> + <valueHelp> + <format>syslog</format> <description>Authentication and authorization</description> </valueHelp> - <valueHelp> - <format>user</format> - <description>Application processes</description> - </valueHelp> - <valueHelp> - <format>uucp</format> - <description>UUCP subsystem</description> - </valueHelp> - <valueHelp> - <format>local0</format> - <description>Local facility 0</description> - </valueHelp> - <valueHelp> - <format>local1</format> - <description>Local facility 1</description> - </valueHelp> - <valueHelp> - <format>local2</format> - <description>Local facility 2</description> - </valueHelp> - <valueHelp> - <format>local3</format> - <description>Local facility 3</description> - </valueHelp> - <valueHelp> - <format>local4</format> - <description>Local facility 4</description> - </valueHelp> - <valueHelp> - <format>local5</format> - <description>Local facility 5</description> - </valueHelp> - <valueHelp> - <format>local6</format> - <description>Local facility 6</description> - </valueHelp> - <valueHelp> - <format>local7</format> - <description>Local facility 7</description> - </valueHelp> - </properties> - <children> - <leafNode name="level"> - <properties> - <help>Logging level</help> - <completionHelp> - <list>emerg alert crit err warning notice info debug all</list> - </completionHelp> - <valueHelp> - <format>emerg</format> + <valueHelp> + <format>user</format> + <description>Application processes</description> + </valueHelp> + <valueHelp> + <format>uucp</format> + <description>UUCP subsystem</description> + </valueHelp> + <valueHelp> + <format>local0</format> + <description>Local facility 0</description> + </valueHelp> + <valueHelp> + <format>local1</format> + <description>Local facility 1</description> + </valueHelp> + <valueHelp> + <format>local2</format> + <description>Local facility 2</description> + </valueHelp> + <valueHelp> + <format>local3</format> + <description>Local facility 3</description> + </valueHelp> + <valueHelp> + <format>local4</format> + <description>Local facility 4</description> + </valueHelp> + <valueHelp> + <format>local5</format> + <description>Local facility 5</description> + </valueHelp> + <valueHelp> + <format>local6</format> + <description>Local facility 6</description> + </valueHelp> + <valueHelp> + <format>local7</format> + <description>Local facility 7</description> + </valueHelp> + </properties> + <children> + <leafNode name="level"> + <properties> + <help>Logging level</help> + <completionHelp> + <list>emerg alert crit err warning notice info debug all</list> + </completionHelp> + <valueHelp> + <format>emerg</format> <description>Emergency messages</description> </valueHelp> <valueHelp> @@ -697,12 +698,12 @@ <format>all</format> <description>Log everything</description> </valueHelp> - </properties> - </leafNode> - </children> - </tagNode> - </children> - </tagNode> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> <node name="console"> <properties> <help>logging to serial console</help> @@ -757,7 +758,7 @@ <valueHelp> <format>protocols</format> <description>depricated will be set to local7</description> - </valueHelp> + </valueHelp> <valueHelp> <format>security</format> <description>depricated will be set to auth</description> diff --git a/interface-definitions/tftp-server.xml b/interface-definitions/tftp-server.xml new file mode 100644 index 000000000..2874b034c --- /dev/null +++ b/interface-definitions/tftp-server.xml @@ -0,0 +1,57 @@ +<?xml version="1.0"?> +<!-- TFTP configuration --> +<interfaceDefinition> + <node name="service"> + <children> + <node name="tftp-server" owner="${vyos_conf_scripts_dir}/tftp_server.py"> + <properties> + <help>Trivial File Transfer Protocol (TFTP) server</help> + <priority>990</priority> + </properties> + <children> + <leafNode name="directory"> + <properties> + <help>Folder containing files served by TFTP [REQUIRED]</help> + </properties> + </leafNode> + <leafNode name="allow-upload"> + <properties> + <help>Allow TFTP file uploads</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Port for TFTP service</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port (default: 69)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="listen-address"> + <properties> + <help>Addresses for TFTP server to listen [REQUIRED]</help> + <valueHelp> + <format>ipv4</format> + <description>TFTP IPv4 listen address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>TFTP IPv6 listen address</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/vrrp.xml b/interface-definitions/vrrp.xml index 72419efe9..2884ef613 100644 --- a/interface-definitions/vrrp.xml +++ b/interface-definitions/vrrp.xml @@ -146,7 +146,7 @@ <properties> <help>Preempt delay (in seconds)</help> <constraint> - <validator name="numeric" argument="--positive"/> + <validator name="numeric" argument="--range 0-1000"/> </constraint> </properties> </leafNode> diff --git a/interface-definitions/wireguard.xml b/interface-definitions/wireguard.xml index 1437e9f0c..5e9c41ace 100644 --- a/interface-definitions/wireguard.xml +++ b/interface-definitions/wireguard.xml @@ -42,42 +42,81 @@ <constraintErrorMessage>interface description is too long (limit 100 characters)</constraintErrorMessage> </properties> </leafNode> - <leafNode name="listen-port"> + <leafNode name="port"> <properties> <help>Local port number to accept connections</help> + <constraint> + <validator name="numeric" argument="--range 1024-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="mtu"> + <properties> + <help>interface mtu size(default: 1420)</help> + <constraint> + <validator name="numeric" argument="--range 68-9000"/> + </constraint> + </properties> + </leafNode> + <leafNode name="fwmark"> + <properties> + <help>A 32-bit fwmark value set on all outgoing packets</help> + <valueHelp> + <format>number</format> + <description>value which marks the packet for QoS/shaper</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> </properties> </leafNode> <tagNode name="peer"> <properties> - <help>Base64 encoded public key</help> + <help>peer alias</help> <constraint> - <regex>^[0-9a-zA-Z\+/]{43}=$</regex> + <regex>.[^ ]{1,100}$</regex> </constraint> - <constraintErrorMessage>Key is not valid 44-character (32-bytes) base64</constraintErrorMessage> + <constraintErrorMessage>peer alias too long (limit 100 characters)</constraintErrorMessage> </properties> <children> + <leafNode name="pubkey"> + <properties> + <help>base64 encoded public key</help> + <constraint> + <regex>^[0-9a-zA-Z\+/]{43}=$</regex> + </constraint> + <constraintErrorMessage>Key is not valid 44-character (32-bytes) base64</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="preshared-key"> + <properties> + <help>base64 encoded preshared key</help> + <constraint> + <regex>^[0-9a-zA-Z\+/]{43}=$</regex> + </constraint> + <constraintErrorMessage>Key is not valid 44-character (32-bytes) base64</constraintErrorMessage> + </properties> + </leafNode> <leafNode name="allowed-ips"> <properties> <help>IP addresses allowed to traverse the peer</help> <multi/> </properties> </leafNode> - <!-- check format IP:port --> + <!-- eventually check format IP:port --> <leafNode name="endpoint"> <properties> - <help>Remote endpoint</help> + <help>Remote endpoint (IP:port)</help> </properties> </leafNode> <leafNode name="persistent-keepalive"> <properties> <help>how often send keep alives in seconds</help> <constraint> - <regex>^(1|[1-9][0-9]{1,5})$</regex> + <validator name="numeric" argument="--range 1-65535"/> </constraint> - <constraintErrorMessage>keepliave timer has to be between 1 and 99999 seconds</constraintErrorMessage> </properties> </leafNode> - </children> </tagNode> </children> diff --git a/op-mode-definitions/dhcp.xml b/op-mode-definitions/dhcp.xml new file mode 100644 index 000000000..eb57f8f1f --- /dev/null +++ b/op-mode-definitions/dhcp.xml @@ -0,0 +1,111 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="dhcp"> + <properties> + <help>Show DHCP (Dynamic Host Configuration Protocol) information</help> + </properties> + <children> + <node name="server"> + <properties> + <help>Show DHCP information</help> + </properties> + <children> + <node name="leases"> + <properties> + <help>Show DHCP server leases</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases</command> + <children> + <tagNode name="pool"> + <properties> + <help>Show DHCP leases for a specific pool</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases --pool $4</command> + </tagNode> + </children> + </node> + <node name="statistics"> + <properties> + <help>Show DHCP server statistics</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --statistics</command> + <children> + <tagNode name="pool"> + <properties> + <help>Show DHCP server statistics for a specific pool</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --statistics --pool $4</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="dhcpv6"> + <properties> + <help>Show DHCPv6 (IPv6 Dynamic Host Configuration Protocol) information</help> + </properties> + <children> + <node name="server"> + <properties> + <help>Show DHCPv6 server information</help> + </properties> + <children> + <node name="leases"> + <properties> + <help>Show DHCPv6 server leases</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases</command> + </node> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="restart"> + <children> + <node name="dhcp"> + <properties> + <help>Restart DHCP processes</help> + </properties> + <children> + <node name="server"> + <properties> + <help>Restart the DHCP server process</help> + </properties> + <command>sudo systemctl restart isc-dhcp-server.service</command> + </node> + <node name="relay-agent"> + <properties> + <help>Restart the DHCP server process</help> + </properties> + <command>sudo /opt/vyatta/sbin/dhcrelay-starter.pl --op-mode --init='/opt/vyatta/sbin/dhcrelay.init'</command> + </node> + </children> + </node> + <node name="dhcpv6"> + <properties> + <help>Restart DHCPv6 processes</help> + </properties> + <children> + <node name="server"> + <properties> + <help>Restart the DHCPv6 server process</help> + </properties> + <command>sudo systemctl restart isc-dhcpv6-server.service</command> + </node> + <node name="relay-agent"> + <properties> + <help>Restart the DHCP server process</help> + </properties> + <command>sudo /opt/vyatta/sbin/dhcv6relay-starter.pl --op_mode --config_action ACTIVE</command> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/force-arp.xml b/op-mode-definitions/force-arp.xml new file mode 100644 index 000000000..3eadabf0a --- /dev/null +++ b/op-mode-definitions/force-arp.xml @@ -0,0 +1,79 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="force"> + <properties> + <help>Force an operation</help> + </properties> + <children> + <node name="arp"> + <properties> + <help>Send gratuitous ARP request or reply</help> + </properties> + <children> + <node name="reply"> + <properties> + <help>Send gratuitous ARP reply</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Send gratuitous ARP reply on specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script> + </completionHelp> + </properties> + <children> + <tagNode name="address"> + <properties> + <help>Send gratuitous ARP reply for specified address</help> + </properties> + <command>sudo arping -I $5 -c 1 -A $7</command> + <children> + <tagNode name="count"> + <properties> + <help>Send specified number of ARP replies</help> + </properties> + <command>sudo arping -I $5 -c $9 -A $7</command> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + <node name="request"> + <properties> + <help>Send gratuitous ARP request</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Send gratuitous ARP request on specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script> + </completionHelp> + </properties> + <children> + <tagNode name="address"> + <properties> + <help>Send gratuitous ARP request for specified address</help> + </properties> + <command>sudo arping -I $5 -c 1 -U $7</command> + <children> + <tagNode name="count"> + <properties> + <help>Send specified number of ARP requests</help> + </properties> + <command>sudo arping -I $5 -c $9 -U $7</command> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/traffic-dump.xml b/op-mode-definitions/traffic-dump.xml index a6810644e..00a809a7c 100644 --- a/op-mode-definitions/traffic-dump.xml +++ b/op-mode-definitions/traffic-dump.xml @@ -8,7 +8,7 @@ </properties> <children> <tagNode name="interface"> - <command>tcpdump -i $4</command> + <command>sudo tcpdump -i $4</command> <properties> <help>Monitor traffic dump from an interface</help> <completionHelp> @@ -17,19 +17,19 @@ </properties> <children> <tagNode name="filter"> - <command>tcpdump -n -i $4 $6</command> + <command>sudo tcpdump -n -i $4 $6</command> <properties> <help>Monitor traffic matching filter conditions</help> </properties> </tagNode> <tagNode name="save"> - <command>tcpdump -n -i $4 -w $6</command> + <command>sudo tcpdump -n -i $4 -w $6</command> <properties> <help>Save traffic dump from an interface to a file</help> </properties> <children> <tagNode name="filter"> - <command>tcpdump -n -i $4 -w $6 $8</command> + <command>sudo tcpdump -n -i $4 -w $6 $8</command> <properties> <help>Save a dump of traffic matching filter conditions to a file</help> </properties> diff --git a/op-mode-definitions/wireguard.xml b/op-mode-definitions/wireguard.xml index 29fce33b6..dd62f0f2f 100644 --- a/op-mode-definitions/wireguard.xml +++ b/op-mode-definitions/wireguard.xml @@ -12,8 +12,14 @@ <properties> <help>generate a wireguard keypair</help> </properties> - <command>${vyos_op_scripts_dir}/wireguard_key.py --genkey</command> + <command>${vyos_op_scripts_dir}/wireguard.py --genkey</command> </leafNode> + <leafNode name="preshared-key"> + <properties> + <help>generate a wireguard preshared key</help> + </properties> + <command>${vyos_op_scripts_dir}/wireguard.py --genpsk</command> + </leafNode> </children> </node> </children> @@ -26,16 +32,50 @@ <properties> <help>show wireguard public key</help> </properties> - <command>${vyos_op_scripts_dir}/wireguard_key.py --showpub</command> + <command>${vyos_op_scripts_dir}/wireguard.py --showpub</command> </leafNode> <leafNode name="privkey"> <properties> <help>show wireguard private key</help> </properties> - <command>${vyos_op_scripts_dir}/wireguard_key.py --showpriv</command> + <command>${vyos_op_scripts_dir}/wireguard.py --showpriv</command> </leafNode> </children> </node> + <node name="interfaces"> + <children> + <tagNode name="wireguard"> + <properties> + <help>show wireguard interface information</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py -t wireguard</script> + </completionHelp> + </properties> + <command>sudo wg show "$4"</command> + <children> + <leafNode name="allowed-ips"> + <properties> + <help>show all allowed-ips for the specified interface</help> + </properties> + <command>sudo wg show "$4" allowed-ips</command> + </leafNode> + <leafNode name="endpoints"> + <properties> + <help>show all endpoints for the specified interface</help> + </properties> + <command>sudo wg show "$4" endpoints</command> + </leafNode> + <leafNode name="peers"> + <properties> + <help>show all peer IDs for the specified interface</help> + </properties> + <command>sudo wg show "$4" peers</command> + </leafNode> + <!-- more commands upon request --> + </children> + </tagNode> + </children> + </node> </children> </node> </interfaceDefinition> diff --git a/python/vyos/config.py b/python/vyos/config.py index 5af830480..bcf04225b 100644 --- a/python/vyos/config.py +++ b/python/vyos/config.py @@ -280,8 +280,8 @@ class Config(object): else: try: out = self._run(self._make_command('returnValues', full_path)) - values = out.split() - return list(map(lambda x: re.sub(r'^\'(.*)\'$', r'\1',x), values)) + values = re.findall(r"\'(.*?)\'", out) + return values except VyOSError: return(default) @@ -309,8 +309,8 @@ class Config(object): if self.is_tag(path): try: out = self._run(self._make_command('listNodes', full_path)) - values = out.split() - return list(map(lambda x: re.sub(r'^\'(.*)\'$', r'\1',x), values)) + values = re.findall(r"\'(.*?)\'", out) + return values except VyOSError: return(default) else: diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index 4b46a1fb3..39fe41669 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -24,6 +24,7 @@ def strip_comments(s): IN_COMMENT = 1 i = len(s) - 1 + state = INITIAL config_end = 0 @@ -42,14 +43,11 @@ def strip_comments(s): break elif (state == INITIAL) and (c == '/'): # A comment begins, or it's a stray slash - try: - if (s[i-1] == '*'): - state = IN_COMMENT - i -= 2 - else: - raise ValueError("Invalid syntax") - except: - raise ValueError("Invalid syntax") + if (s[i-1] == '*'): + state = IN_COMMENT + i -= 2 + else: + raise ValueError("Invalid syntax: stray slash at character {0}".format(i + 1)) elif (state == INITIAL) and (c == '}'): # We are not inside a comment, that's the end of the last node config_end = i + 1 @@ -61,12 +59,12 @@ def strip_comments(s): state = INITIAL i -= 2 except: - raise ValueError("Invalid syntax") + raise ValueError("Invalid syntax: malformed commend end at character {0}".format(i + 1)) elif (state == IN_COMMENT) and (c != '*'): # Ignore everything inside comments, including braces i -= 1 else: - raise ValueError("Invalid syntax") + raise ValueError("Invalid syntax at character {0}: invalid character {1}".format(i + 1, c)) return (s[0:config_end], s[config_end+1:]) @@ -92,6 +90,10 @@ class ConfigTree(object): self.__from_string.argtypes = [c_char_p] self.__from_string.restype = c_void_p + self.__get_error = self.__lib.get_error + self.__get_error.argtypes = [] + self.__get_error.restype = c_char_p + self.__to_string = self.__lib.to_string self.__to_string.argtypes = [c_void_p] self.__to_string.restype = c_char_p @@ -112,6 +114,10 @@ class ConfigTree(object): self.__delete.argtypes = [c_void_p, c_char_p] self.__delete.restype = c_int + self.__rename = self.__lib.rename_node + self.__rename.argtypes = [c_void_p, c_char_p, c_char_p] + self.__rename.restype = c_int + self.__set_replace_value = self.__lib.set_replace_value self.__set_replace_value.argtypes = [c_void_p, c_char_p, c_char_p] self.__set_replace_value.restype = c_int @@ -150,10 +156,12 @@ class ConfigTree(object): config_section, comments_section = strip_comments(config_string) config = self.__from_string(config_section.encode()) if config is None: - raise ValueError("Parse error") + msg = self.__get_error().decode() + raise ValueError("Failed to parse config: {0}".format(msg)) else: self.__config = config self.__comments = comments_section + def __del__(self): if self.__config is not None: self.__destroy(self.__config) @@ -193,6 +201,13 @@ class ConfigTree(object): self.__delete_value(self.__config, path_str, value.encode()) + def rename(self, path, newname): + check_path(path) + path_str = " ".join(map(str, path)).encode() + newname_str = newname.encode() + + self.__rename(self.__config, path_str, newname_str) + def exists(self, path): check_path(path) path_str = " ".join(map(str, path)).encode() diff --git a/python/vyos/validate.py b/python/vyos/validate.py new file mode 100644 index 000000000..1b77f196a --- /dev/null +++ b/python/vyos/validate.py @@ -0,0 +1,100 @@ +# Copyright 2018 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import netifaces +import ipaddress + +def is_ipv4(addr): + """ + Check addr if it is an IPv4 address/network. + + Return True/False + """ + if ipaddress.ip_network(addr).version == 4: + return True + else: + return False + +def is_ipv6(addr): + """ + Check addr if it is an IPv6 address/network. + + Return True/False + """ + if ipaddress.ip_network(addr).version == 6: + return True + else: + return False + +def is_addr_assigned(addr): + """ + Verify if the given IPv4/IPv6 address is assigned to any interface on this + system. + + Return True/False + """ + + # determine IP version (AF_INET or AF_INET6) depending on passed address + addr_type = netifaces.AF_INET + if is_ipv6(addr): + addr_type = netifaces.AF_INET6 + + for interface in netifaces.interfaces(): + # check if the requested address type is configured at all + if addr_type in netifaces.ifaddresses(interface).keys(): + # Check every IP address on this interface for a match + for ip in netifaces.ifaddresses(interface)[addr_type]: + # Check if it matches to the address requested + if ip['addr'] == addr: + return True + + return False + +def is_subnet_connected(subnet, primary=False): + """ + Verify is the given IPv4/IPv6 subnet is connected to any interface on this + system. + + primary check if the subnet is reachable via the primary IP address of this + interface, or in other words has a broadcast address configured. ISC DHCP + for instance will complain if it should listen on non broadcast interfaces. + + Return True/False + """ + + # determine IP version (AF_INET or AF_INET6) depending on passed address + addr_type = netifaces.AF_INET + if is_ipv6(subnet): + addr_type = netifaces.AF_INET6 + + for interface in netifaces.interfaces(): + # check if the requested address type is configured at all + if addr_type not in netifaces.ifaddresses(interface).keys(): + continue + + # An interface can have multiple addresses, but some software components + # only support the primary address :( + if primary: + ip = netifaces.ifaddresses(interface)[addr_type][0]['addr'] + if ipaddress.ip_address(ip) in ipaddress.ip_network(subnet): + return True + else: + # Check every assigned IP address if it is connected to the subnet + # in question + for ip in netifaces.ifaddresses(interface)[addr_type]: + if ipaddress.ip_address(ip['addr']) in ipaddress.ip_network(subnet): + return True + + return False diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py index 95f6215b5..d1257d4a5 100755 --- a/src/conf_mode/bcast_relay.py +++ b/src/conf_mode/bcast_relay.py @@ -19,56 +19,104 @@ import sys import os import fnmatch -import subprocess +import jinja2 from vyos.config import Config from vyos import ConfigError config_file = r'/etc/default/udp-broadcast-relay' +config_tmpl = """ +### Autogenerated by bcast_relay.py ### + +# UDP broadcast relay configuration for instance {{ id }} +{%- if description %} +# Comment: {{ description }} +{% endif -%} +DAEMON_ARGS="{% if address %}-s {{ address }} {% endif %}{{ id }} {{ port }} {{ interfaces | join(' ') }}" +""" + +default_config_data = { + 'disabled': False, + 'instances': [] +} + def get_config(): + relay = default_config_data conf = Config() - conf.set_level("service broadcast-relay id") - relay_id = conf.list_nodes("") - relays = [] - - for id in relay_id: - interface_list = [] - address = conf.return_value("{0} address".format(id)) - description = conf.return_value("{0} description".format(id)) - port = conf.return_value("{0} port".format(id)) - - # split the interface name listing and form a list - if conf.exists("{0} interface".format(id)): - intfs_names = [] - intfs_names = conf.return_values("{0} interface".format(id)) - - for name in intfs_names: - interface_list.append(name) - - relay = { - "id": id, - "address": address, - "description": description, - "interfaces" : interface_list, - "port": port + if not conf.exists('service broadcast-relay'): + return None + else: + conf.set_level('service broadcast-relay') + + # Service can be disabled by user + if conf.exists('disable'): + relay['disabled'] = True + return relay + + # Parse configuration of each individual instance + if conf.exists('id'): + for id in conf.list_nodes('id'): + conf.set_level('service broadcast-relay id {0}'.format(id)) + config = { + 'id': id, + 'disabled': False, + 'address': '', + 'description': '', + 'interfaces': [], + 'port': '' } - relays.append(relay) - return relays + # Check if individual broadcast relay service is disabled + if conf.exists('disable'): + config['disabled'] = True + + # Source IP of forwarded packets, if empty original senders address is used + if conf.exists('address'): + config['address'] = conf.return_value('address') + + # A description for each individual broadcast relay service + if conf.exists('description'): + config['description'] = conf.return_value('description') + + # UDP port to listen on for broadcast frames + if conf.exists('port'): + config['port'] = conf.return_value('port') + + # Network interfaces to listen on for broadcast frames to be relayed + if conf.exists('interface'): + config['interfaces'] = conf.return_values('interface') + + relay['instances'].append(config) -def verify(relays): - for relay in relays: - if not relay["port"]: - raise ConfigError("UDP broadcast relay 'id {0}' requires a port number".format(relay["id"])) + return relay - if len(relay["interfaces"]) < 2: - raise ConfigError("UDP broadcast relay 'id {0}' requires at least 2 interfaces".format(relay["id"])) +def verify(relay): + if relay is None: + return None + + if relay['disabled']: + return None + + for r in relay['instances']: + # we don't have to check this instance when it's disabled + if r['disabled']: + continue + + # we certainly require a UDP port to listen to + if not r['port']: + raise ConfigError('UDP broadcast relay "{0}" requires a port number'.format(r['id'])) + + # Relaying data without two interface is kinda senseless ... + if len(r['interfaces']) < 2: + raise ConfigError('UDP broadcast relay "id {0}" requires at least 2 interfaces'.format(r['id'])) return None -def generate(relays): - config_header = '### Autogenerated by bcast_relay.py ###\n' + +def generate(relay): + if relay is None: + return None config_dir = os.path.dirname(config_file) config_filename = os.path.basename(config_file) @@ -82,32 +130,43 @@ def generate(relays): # sort our list active_configs.sort() + # delete old configuration files for id in active_configs[:]: - os.unlink(config_file + id) - - for relay in relays: - file = config_file + str(relay["id"]) - interfaces = ' '.join(str(intf) for intf in relay["interfaces"]) - config_args = 'DAEMON_ARGS="{0} {1}"\n'.format(relay["port"], interfaces) - - f = open(file, 'w') - f.write(config_header) - if relay["description"]: - f.write('# ' + relay["description"] + '\n') - f.write(config_args) - f.close() + if os.path.exists(config_file + id): + os.unlink(config_file + id) + + # If the service is disabled, we can bail out here + if relay['disabled']: + print('Warning: UDP broadcast relay service will be deactivated because it is disabled') + return None + + for r in relay['instances']: + # Skip writing instance config when it's disabled + if r['disabled']: + continue + + # configuration filename contains instance id + file = config_file + str(r['id']) + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(r) + with open(file, 'w') as f: + f.write(config_text) return None -def apply(relays): +def apply(relay): # first stop all running services - cmd = "sudo systemctl stop udp-broadcast-relay@{1..99}" - os.system(cmd) + os.system('sudo systemctl stop udp-broadcast-relay@{1..99}') + + if (relay is None) or relay['disabled']: + return None # start only required service instances - for relay in relays: - cmd = "sudo systemctl start udp-broadcast-relay@{0}".format(relay["id"]) - os.system(cmd) + for r in relay['instances']: + # Don't start individual instance when it's disabled + if r['disabled']: + continue + os.system('sudo systemctl start udp-broadcast-relay@{0}'.format(r['id'])) return None diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py new file mode 100755 index 000000000..2a2b1fe6c --- /dev/null +++ b/src/conf_mode/dhcp_server.py @@ -0,0 +1,804 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import os +import ipaddress +import jinja2 +import socket +import struct + +import vyos.validate + +from vyos.config import Config +from vyos import ConfigError + +config_file = r'/etc/dhcp/dhcpd.conf' +lease_file = r'/config/dhcpd.leases' +daemon_config_file = r'/etc/default/isc-dhcp-server' + +# Please be careful if you edit the template. +config_tmpl = """ +### Autogenerated by dhcp_server.py ### + +# For options please consult the following website: +# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html +# +# log-facility local7; + +{% if hostfile_update %} +on commit { + set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); + set ClientIp = binary-to-ascii(10, 8, ".", leased-address); + set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6)); + set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); + execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "commit", ClientName, ClientIp, ClientMac, ClientDomain); +} + +on release { + set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); + set ClientIp = binary-to-ascii(10, 8, ".",leased-address); + set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6)); + set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); + execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain); +} + +on expiry { + set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); + set ClientIp = binary-to-ascii(10, 8, ".",leased-address); + set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6)); + set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); + execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain); +} +{% endif %} +ddns-update-style {% if ddns_enable -%} interim {%- else -%} none {%- endif %}; +{% if static_route -%} +option rfc3442-static-route code 121 = array of integer 8; +option windows-static-route code 249 = array of integer 8; +{%- endif %} +{% if static_route -%} +option wpad-url code 252 = text; +{% endif %} + +{%- if global_parameters %} +# The following {{ global_parameters | length }} line(s) were added as global-parameters in the CLI and have not been validated +{%- for param in global_parameters %} +{{ param }} +{%- endfor -%} +{%- endif %} + +# Failover configuration +{% for network in shared_network %} +{%- if not network.disabled -%} +{%- for subnet in network.subnet %} +{%- if subnet.failover_name -%} +failover peer "{{ subnet.failover_name }}" { +{%- if subnet.failover_status == 'primary' %} + primary; + mclt 1800; + split 128; +{%- elif subnet.failover_status == 'secondary' %} + secondary; +{%- endif %} + address {{ subnet.failover_local_addr }}; + port 520; + peer address {{ subnet.failover_peer_addr }}; + peer port 520; + max-response-delay 30; + max-unacked-updates 10; + load balance max seconds 3; +} +{% endif -%} +{% endfor -%} +{% endif -%} +{% endfor %} + +# Shared network configration(s) +{% for network in shared_network %} +{%- if not network.disabled -%} +shared-network {{ network.name }} { + {{ "authoritative;" if network.authoritative }} + {%- if network.network_parameters %} + # The following {{ network.network_parameters | length }} line(s) were added as shared-network-parameters in the CLI and have not been validated + {%- for param in network.network_parameters %} + {{ param }} + {%- endfor %} + {%- endif %} + {%- for subnet in network.subnet %} + subnet {{ subnet.address }} netmask {{ subnet.netmask }} { + {%- if subnet.dns_server %} + option domain-name-servers {{ subnet.dns_server | join(', ') }}; + {%- endif %} + {%- if subnet.domain_search %} + option domain-search {{ subnet.domain_search | join(', ') }}; + {%- endif %} + {%- if subnet.ntp_server %} + option ntp-servers {{ subnet.ntp_server | join(', ') }}; + {%- endif %} + {%- if subnet.pop_server %} + option pop-server {{ subnet.pop_server | join(', ') }}; + {%- endif %} + {%- if subnet.smtp_server %} + option smtp-server {{ subnet.smtp_server | join(', ') }}; + {%- endif %} + {%- if subnet.time_server %} + option time-servers {{ subnet.time_server | join(', ') }}; + {%- endif %} + {%- if subnet.wins_server %} + option netbios-name-servers {{ subnet.wins_server | join(', ') }}; + {%- endif %} + {%- if subnet.static_route %} + option rfc3442-static-route {{ subnet.static_route }}; + option windows-static-route {{ subnet.static_route }}; + {%- endif %} + {%- if subnet.ip_forwarding %} + option ip-forwarding true; + {%- endif -%} + {%- if subnet.default_router %} + option routers {{ subnet.default_router }}; + {%- endif -%} + {%- if subnet.server_identifier %} + option dhcp-server-identifier {{ subnet.server_identifier }}; + {%- endif -%} + {%- if subnet.domain_name %} + option domain-name "{{ subnet.domain_name }}"; + {%- endif -%} + {%- if subnet.tftp_server %} + option tftp-server-name "{{ subnet.tftp_server }}"; + {%- endif -%} + {%- if subnet.bootfile_name %} + option bootfile-name "{{ subnet.bootfile_name }}"; + filename "{{ subnet.bootfile_name }}"; + {%- endif -%} + {%- if subnet.bootfile_server %} + next-server {{ subnet.bootfile_server }}; + {%- endif -%} + {%- if subnet.time_offset %} + option time-offset {{ subnet.time_offset }}; + {%- endif -%} + {%- if subnet.wpad_url %} + option wpad-url "{{ subnet.wpad_url }}"; + {%- endif -%} + {%- if subnet.client_prefix_length %} + option subnet-mask {{ subnet.client_prefix_length }}; + {%- endif -%} + {% if subnet.lease %} + default-lease-time {{ subnet.lease }}; + max-lease-time {{ subnet.lease }}; + {%- endif -%} + {%- for host in subnet.static_mapping %} + {% if not host.disabled -%} + host {{ network.name }}_{{ host.name }} { + fixed-address {{ host.ip_address }}; + hardware ethernet {{ host.mac_address }}; + {%- if host.static_parameters %} + # The following {{ host.static_parameters | length }} line(s) were added as static-mapping-parameters in the CLI and have not been validated + {%- for param in host.static_parameters %} + {{ param }} + {%- endfor -%} + {%- endif %} + } + {%- endif %} + {%- endfor %} + {%- if subnet.failover_name %} + pool { + failover peer "{{ subnet.failover_name }}"; + deny dynamic bootp clients; + {%- for range in subnet.range %} + range {{ range.start }} {{ range.stop }}; + {%- endfor %} + } + {%- else %} + {%- for range in subnet.range %} + range {{ range.start }} {{ range.stop }}; + {%- endfor %} + {%- endif %} + } + {%- endfor %} + on commit { set shared-networkname = "{{ network.name }}"; } +} +{%- endif %} +{% endfor %} +""" + +daemon_tmpl = """ +### Autogenerated by dhcp_server.py ### + +# sourced by /etc/init.d/isc-dhcp-server + +DHCPD_CONF=/etc/dhcp/dhcpd.conf +DHCPD_PID=/var/run/dhcpd.pid +OPTIONS="-4 -lf {{ lease_file }}" +INTERFACES="" +""" + +default_config_data = { + 'lease_file': lease_file, + 'disabled': False, + 'ddns_enable': False, + 'global_parameters': [], + 'hostfile_update': False, + 'static_route': False, + 'wpad': False, + 'shared_network': [], +} + +def get_config(): + dhcp = default_config_data + conf = Config() + if not conf.exists('service dhcp-server'): + return None + else: + conf.set_level('service dhcp-server') + + # check for global disable of DHCP service + if conf.exists('disable'): + dhcp['disabled'] = True + + # check for global dynamic DNS upste + if conf.exists('dynamic-dns-update'): + dhcp['ddns_enable'] = True + + # HACKS AND TRICKS + # + # check for global 'raw' ISC DHCP parameters configured by users + # actually this is a bad idea in general to pass raw parameters from any user + if conf.exists('global-parameters'): + dhcp['global_parameters'] = conf.return_values('global-parameters') + + # check for global DHCP server updating /etc/host per lease + if conf.exists('hostfile-update'): + dhcp['hostfile_update'] = True + + # check for multiple, shared networks served with DHCP addresses + if conf.exists('shared-network-name'): + for network in conf.list_nodes('shared-network-name'): + conf.set_level('service dhcp-server shared-network-name {0}'.format(network)) + config = { + 'name': network, + 'authoritative': False, + 'description': '', + 'disabled': False, + 'network_parameters': [], + 'subnet': [] + } + # check if DHCP server should be authoritative on this network + if conf.exists('authoritative'): + config['authoritative'] = True + + # A description for this given network + if conf.exists('description'): + config['description'] = conf.return_value('description') + + # If disabled, the shared-network configuration becomes inactive in + # the running DHCP server instance + if conf.exists('disable'): + config['disabled'] = True + + # HACKS AND TRICKS + # + # check for 'raw' ISC DHCP parameters configured by users + # actually this is a bad idea in general to pass raw parameters + # from any user + # + # deprecate this and issue a warning like we do for DNS forwarding? + if conf.exists('shared-network-parameters'): + config['network_parameters'] = conf.return_values('shared-network-parameters') + + # check for multiple subnet configurations in a shared network + # config segment + if conf.exists('subnet'): + for net in conf.list_nodes('subnet'): + conf.set_level('service dhcp-server shared-network-name {0} subnet {1}'.format(network, net)) + subnet = { + 'network': net, + 'address': str(ipaddress.ip_network(net).network_address), + 'netmask': str(ipaddress.ip_network(net).netmask), + 'bootfile_name': '', + 'bootfile_server': '', + 'client_prefix_length': '', + 'default_router': '', + 'dns_server': [], + 'domain_name': '', + 'domain_search': [], + 'exclude': [], + 'failover_local_addr': '', + 'failover_name': '', + 'failover_peer_addr': '', + 'failover_status': '', + 'ip_forwarding': False, + 'lease': '86400', + 'ntp_server': [], + 'pop_server': [], + 'server_identifier': '', + 'smtp_server': [], + 'range': [], + 'static_mapping': [], + 'static_subnet': '', + 'static_router': '', + 'static_route': '', + 'subnet_parameters': [], + 'tftp_server': '', + 'time_offset': '', + 'time_server': [], + 'wins_server': [], + 'wpad_url': '' + } + + # Used to identify a bootstrap file + if conf.exists('bootfile-name'): + subnet['bootfile_name'] = conf.return_value('bootfile-name') + + # Specify host address of the server from which the initial boot file + # (specified above) is to be loaded. Should be a numeric IP address or + # domain name. + if conf.exists('bootfile-server'): + subnet['bootfile_server'] = conf.return_value('bootfile-server') + + # The subnet mask option specifies the client's subnet mask as per RFC 950. If no subnet + # mask option is provided anywhere in scope, as a last resort dhcpd will use the subnet + # mask from the subnet declaration for the network on which an address is being assigned. + if conf.exists('client-prefix-length'): + # snippet borrowed from https://stackoverflow.com/questions/33750233/convert-cidr-to-subnet-mask-in-python + host_bits = 32 - int(conf.return_value('client-prefix-length')) + subnet['client_prefix_length'] = socket.inet_ntoa(struct.pack('!I', (1 << 32) - (1 << host_bits))) + + # Default router IP address on the client's subnet + if conf.exists('default-router'): + subnet['default_router'] = conf.return_value('default-router') + + # Specifies a list of Domain Name System (STD 13, RFC 1035) name servers available to + # the client. Servers should be listed in order of preference. + if conf.exists('dns-server'): + subnet['dns_server'] = conf.return_values('dns-server') + + # Option specifies the domain name that client should use when resolving hostnames + # via the Domain Name System. + if conf.exists('domain-name'): + subnet['domain_name'] = conf.return_value('domain-name') + + # The domain-search option specifies a 'search list' of Domain Names to be used + # by the client to locate not-fully-qualified domain names. + if conf.exists('domain-search'): + for domain in conf.return_values('domain-search'): + subnet['domain_search'].append('"' + domain + '"') + + # IP address (local) for failover peer to connect + if conf.exists('failover local-address'): + subnet['failover_local_addr'] = conf.return_value('failover local-address') + + # DHCP failover peer name + if conf.exists('failover name'): + subnet['failover_name'] = conf.return_value('failover name') + + # IP address (remote) of failover peer + if conf.exists('failover peer-address'): + subnet['failover_peer_addr'] = conf.return_value('failover peer-address') + + # DHCP failover peer status (primary|secondary) + if conf.exists('failover status'): + subnet['failover_status'] = conf.return_value('failover status') + + # Option specifies whether the client should configure its IP layer for packet + # forwarding + if conf.exists('ip-forwarding'): + subnet['ip_forwarding'] = True + + # Time should be the length in seconds that will be assigned to a lease if the + # client requesting the lease does not ask for a specific expiration time + if conf.exists('lease'): + subnet['lease'] = conf.return_value('lease') + + # Specifies a list of IP addresses indicating NTP (RFC 5905) servers available + # to the client. + if conf.exists('ntp-server'): + subnet['ntp_server'] = conf.return_values('ntp-server') + + # POP3 server option specifies a list of POP3 servers available to the client. + # Servers should be listed in order of preference. + if conf.exists('pop-server'): + subnet['pop_server'] = conf.return_values('pop-server') + + # DHCP servers include this option in the DHCPOFFER in order to allow the client + # to distinguish between lease offers. DHCP clients use the contents of the + # 'server identifier' field as the destination address for any DHCP messages + # unicast to the DHCP server + if conf.exists('server-identifier'): + subnet['server_identifier'] = conf.return_value('server-identifier') + + # SMTP server option specifies a list of SMTP servers available to the client. + # Servers should be listed in order of preference. + if conf.exists('smtp-server'): + subnet['smtp_server'] = conf.return_values('smtp-server') + + # For any subnet on which addresses will be assigned dynamically, there must be at + # least one range statement. The range statement gives the lowest and highest IP + # addresses in a range. All IP addresses in the range should be in the subnet in + # which the range statement is declared. + if conf.exists('range'): + for range in conf.list_nodes('range'): + range = { + 'start': conf.return_value('range {0} start'.format(range)), + 'stop': conf.return_value('range {0} stop'.format(range)) + } + subnet['range'].append(range) + + # IP address that needs to be excluded from DHCP lease range + if conf.exists('exclude'): + # We have no need to store the exclude addresses. Exclude addresses + # are recalculated into several ranges + exclude = [] + subnet['exclude'] = conf.return_values('exclude') + for addr in subnet['exclude']: + exclude.append(ipaddress.ip_address(addr)) + + # sort excluded IP addresses ascending + exclude = sorted(exclude) + + # calculate multipe ranges based on the excluded IP addresses + output = [] + for range in subnet['range']: + range_start = range['start'] + range_stop = range['stop'] + + for i in exclude: + # Excluded IP address must be in out specified range + if (i >= ipaddress.ip_address(range_start)) and (i <= ipaddress.ip_address(range_stop)): + # Build up new IP address range ending one IP address before + # our exclude address + range = { + 'start': str(range_start), + 'stop': str(i - 1) + } + # Our next IP address range will start one address after + # our exclude address + range_start = i + 1 + output.append(range) + + # Take care of last IP address range spanning from the last exclude + # address (+1) to the end of the initial configured range + if i is exclude[-1]: + last = { + 'start': str(i + 1), + 'stop': str(range_stop) + } + output.append(last) + else: + # IP address not inside search range, take range is it is + output.append(range) + + # We successfully build up a new list containing several IP address + # ranges, replace IP address range in our dictionary + subnet['range'] = output + + # Static DHCP leases + if conf.exists('static-mapping'): + for mapping in conf.list_nodes('static-mapping'): + conf.set_level('service dhcp-server shared-network-name {0} subnet {1} static-mapping {2}'.format(network, net, mapping)) + mapping = { + 'name': mapping, + 'disabled': False, + 'ip_address': '', + 'mac_address': '', + 'static_parameters': [] + } + + # This static lease is disabled + if conf.exists('disable'): + mapping['disabled'] = True + + # IP address used for this DHCP client + if conf.exists('ip-address'): + mapping['ip_address'] = conf.return_value('ip-address') + + # MAC address of requesting DHCP client + if conf.exists('mac-address'): + mapping['mac_address'] = conf.return_value('mac-address') + + # HACKS AND TRICKS + # + # check for 'raw' ISC DHCP parameters configured by users + # actually this is a bad idea in general to pass raw parameters + # from any user + # + # deprecate this and issue a warning like we do for DNS forwarding? + if conf.exists('static-mapping-parameters'): + mapping['static_parameters'] = conf.return_values('static-mapping-parameters') + + # append static-mapping configuration to subnet list + subnet['static_mapping'].append(mapping) + + # Reset config level to matching hirachy + conf.set_level('service dhcp-server shared-network-name {0} subnet {1}'.format(network, net)) + + # This option specifies a list of static routes that the client should install in its routing + # cache. If multiple routes to the same destination are specified, they are listed in descending + # order of priority. + if conf.exists('static-route destination-subnet'): + subnet['static_subnet'] = conf.return_value('static-route destination-subnet') + # Required for global config section + dhcp['static_route'] = True + + if conf.exists('static-route router'): + subnet['static_router'] = conf.return_value('static-route router') + + if subnet['static_router'] and subnet['static_subnet']: + # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server + # Option format is: + # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3> + # where bytes with the value 0 are omitted. + net = ipaddress.ip_network(subnet['static_subnet']) + # add netmask + string = str(net.prefixlen) + ',' + # add network bytes + bytes = str(net.network_address).split('.') + for b in bytes: + if b != '0': + string += b + ',' + + # add router bytes + bytes = subnet['static_router'].split('.') + for b in bytes: + if b != '0': + string += b + if b is not bytes[-1]: + string += ',' + + subnet['static_route'] = string + + # HACKS AND TRICKS + # + # check for 'raw' ISC DHCP parameters configured by users + # actually this is a bad idea in general to pass raw parameters + # from any user + # + # deprecate this and issue a warning like we do for DNS forwarding? + if conf.exists('subnet-parameters'): + config['subnet_parameters'] = conf.return_values('subnet-parameters') + + # This option is used to identify a TFTP server and, if supported by the client, should have + # the same effect as the server-name declaration. BOOTP clients are unlikely to support this + # option. Some DHCP clients will support it, and others actually require it. + if conf.exists('tftp-server-name'): + subnet['tftp_server'] = conf.return_value('tftp-server-name') + + # The time-offset option specifies the offset of the client’s subnet in seconds from + # Coordinated Universal Time (UTC). + if conf.exists('time-offset'): + subnet['time_offset'] = conf.return_value('time-offset') + + # The time-server option specifies a list of RFC 868 time servers available to the client. + # Servers should be listed in order of preference. + if conf.exists('time-server'): + subnet['time_server'] = conf.return_values('time-server') + + # The NetBIOS name server (NBNS) option specifies a list of RFC 1001/1002 NBNS name servers + # listed in order of preference. NetBIOS Name Service is currently more commonly referred to + # as WINS. WINS servers can be specified using the netbios-name-servers option. + if conf.exists('wins-server'): + subnet['wins_server'] = conf.return_values('wins-server') + + # URL for Web Proxy Autodiscovery Protocol + if conf.exists('wpad-url'): + subnet['wpad_url'] = conf.return_value('wpad-url') + # Required for global config section + dhcp['wpad'] = True + + # append subnet configuration to shared network subnet list + config['subnet'].append(subnet) + + # append shared network configuration to config dictionary + dhcp['shared_network'].append(config) + + return dhcp + +def verify(dhcp): + if (dhcp is None) or (dhcp['disabled'] is True): + return None + + # If DHCP is enabled we need one share-network + if len(dhcp['shared_network']) == 0: + raise ConfigError('No DHCP shared networks configured.\n' \ + 'At least one DHCP shared network must be configured.') + + # Inspect shared-network/subnet + failover_names = [] + listen_ok = False + subnets = [] + + # A shared-network requires a subnet definition + for network in dhcp['shared_network']: + if len(network['subnet']) == 0: + raise ConfigError('No DHCP lease subnets configured for {0}. At least one\n' \ + 'lease subnet must be configured for each shared network.'.format(network['name'])) + + for subnet in network['subnet']: + # Subnet static route declaration requires destination and router + if subnet['static_subnet'] or subnet['static_router']: + if not (subnet['static_subnet'] and subnet['static_router']): + raise ConfigError('Please specify missing DHCP static-route parameter(s):\n' \ + 'destination-subnet | router') + + # Failover requires all 4 parameters set + if subnet['failover_local_addr'] or subnet['failover_peer_addr'] or subnet['failover_name'] or subnet['failover_status']: + if not (subnet['failover_local_addr'] and subnet['failover_peer_addr'] and subnet['failover_name'] and subnet['failover_status']): + raise ConfigError('Please specify missing DHCP failover parameter(s):\n' \ + 'local-address | peer-address | name | status') + + # Failover names must be uniquie + if subnet['failover_name'] in failover_names: + raise ConfigError('Failover names must be unique:\n' \ + '{0} has already been configured!'.format(subnet['failover_name'])) + else: + failover_names.append(subnet['failover_name']) + + # Failover requires start/stop ranges for pool + if (len(subnet['range']) == 0): + raise ConfigError('At least one start-stop range must be configured for {0}\n' \ + 'to set up DHCP failover!'.format(subnet['network'])) + + # Check if DHCP address range is inside configured subnet declaration + range_start = [] + range_stop = [] + for range in subnet['range']: + start = range['start'] + stop = range['stop'] + # DHCP stop IP required after start IP + if start and not stop: + raise ConfigError('DHCP range stop address for start {0} is not defined!'.format(start)) + + # Start address must be inside network + if not ipaddress.ip_address(start) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCP range start address {0} is not in subnet {1}\n' \ + 'specified for shared network {2}!'.format(start, subnet['network'], network['name'])) + + # Stop address must be inside network + if not ipaddress.ip_address(stop) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCP range stop address {0} is not in subnet {1}\n' \ + 'specified for shared network {2}!'.format(stop, subnet['network'], network['name'])) + + # Stop address must be greater or equal to start address + if not ipaddress.ip_address(stop) >= ipaddress.ip_address(start): + raise ConfigError('DHCP range stop address {0} must be greater or equal\n' \ + 'to the range start address {1}!'.format(stop, start)) + + # Range start address must be unique + if start in range_start: + raise ConfigError('Conflicting DHCP lease range:\n' \ + 'Pool start address {0} defined multipe times!'.format(start)) + else: + range_start.append(start) + + # Range stop address must be unique + if stop in range_stop: + raise ConfigError('Conflicting DHCP lease range:\n' \ + 'Pool stop address {0} defined multipe times!'.format(stop)) + else: + range_stop.append(stop) + + # Exclude addresses must be in bound + for exclude in subnet['exclude']: + if not ipaddress.ip_address(exclude) in ipaddress.ip_network(subnet['network']): + raise ConfigError('Exclude IP address {0} is outside of the DHCP lease network {1}\n' \ + 'under shared network {2}!'.format(exclude, subnet['network'], network['name'])) + + # At least one DHCP address range or static-mapping required + active_mapping = False + if (len(subnet['range']) == 0): + for mapping in subnet['static_mapping']: + # we need at least one active mapping + if (not active_mapping) and (not mapping['disabled']): + active_mapping = True + else: + active_mapping = True + + if not active_mapping: + raise ConfigError('No DHCP address range or active static-mapping set\n' \ + 'for subnet {0}!'.format(subnet['network'])) + + # Static IP address mappings require both an IP address and MAC address + for mapping in subnet['static_mapping']: + # Static IP address must be configured + if not mapping['ip_address']: + raise ConfigError('DHCP static lease IP address not specified for static mapping\n' \ + '{0} under shared network name {1}!'.format(mapping['name'], network['name'])) + + # Static IP address must be in bound + if not ipaddress.ip_address(mapping['ip_address']) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCP static lease IP address {0} for static mapping {1}\n' \ + 'in shared network {2} is outside DHCP lease subnet {3}!' \ + .format(mapping['ip_address'], mapping['name'], network['name'], subnet['network'])) + + # Static mapping requires MAC address + if not mapping['mac_address']: + raise ConfigError('DHCP static lease MAC address not specified for static mapping\n' \ + '{0} under shared network name {1}!'.format(mapping['name'], network['name'])) + + # There must be one subnet connected to a listen interface. + # This only counts if the network itself is not disabled! + if not network['disabled']: + if vyos.validate.is_subnet_connected(subnet['network'], primary=True): + listen_ok = True + + # Subnets must be non overlapping + if subnet['network'] in subnets: + raise ConfigError('DHCP subnets must be unique! Subnet {0} defined multiple times!'.format(subnet)) + else: + subnets.append(subnet['network']) + + # Check for overlapping subnets + net = ipaddress.ip_network(subnet['network']) + for n in subnets: + net2 = ipaddress.ip_network(n) + if (net != net2): + if net.overlaps(net2): + raise ConfigError('DHCP conflicting subnet ranges: {0} overlaps {1}'.format(net, net2)) + + if not listen_ok: + raise ConfigError('None of the DHCP lease subnets are inside any configured subnet on\n' \ + 'broadcast interfaces. At least one lease subnet must be set such that\n' \ + 'DHCP server listens on a one broadcast interface!') + + return None + +def generate(dhcp): + if dhcp is None: + return None + + if dhcp['disabled'] is True: + print('Warning: DHCP server will be deactivated because it is disabled') + return None + + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(dhcp) + with open(config_file, 'w') as f: + f.write(config_text) + + tmpl = jinja2.Template(daemon_tmpl) + config_text = tmpl.render(dhcp) + with open(daemon_config_file, 'w') as f: + f.write(config_text) + + return None + +def apply(dhcp): + if (dhcp is None) or dhcp['disabled']: + # DHCP server is removed in the commit + os.system('sudo systemctl stop isc-dhcp-server.service') + if os.path.exists(config_file): + os.unlink(config_file) + if os.path.exists(daemon_config_file): + os.unlink(daemon_config_file) + else: + # If our file holding DHCP leases does yet not exist - create it + if not os.path.exists(lease_file): + os.mknod(lease_file) + + os.system('sudo systemctl restart isc-dhcp-server.service') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py new file mode 100755 index 000000000..bb3e6e90d --- /dev/null +++ b/src/conf_mode/dhcpv6_server.py @@ -0,0 +1,451 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import os +import ipaddress + +import jinja2 + +import vyos.validate + +from vyos.config import Config +from vyos import ConfigError + +config_file = r'/etc/dhcp/dhcpd6.conf' +lease_file = r'/config/dhcpd6.leases' +daemon_config_file = r'/etc/default/isc-dhcpv6-server' + +# Please be careful if you edit the template. +config_tmpl = """ +### Autogenerated by dhcpv6_server.py ### + +# For options please consult the following website: +# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html + +log-facility local7; +{%- if preference %} +option dhcp6.preference {{ preference }}; +{%- endif %} + +# Shared network configration(s) +{% for network in shared_network %} +{%- if not network.disabled -%} +shared-network {{ network.name }} { + {%- for subnet in network.subnet %} + subnet6 {{ subnet.network }} { + {%- for range in subnet.range6_prefix %} + range6 {{ range.prefix }}{{ " temporary" if range.temporary }}; + {%- endfor %} + {%- for range in subnet.range6 %} + range6 {{ range.start }} {{ range.stop }}; + {%- endfor %} + {%- if subnet.domain_search %} + option dhcp6.domain-search {{ subnet.domain_search | join(', ') }}; + {%- endif %} + {%- if subnet.lease_def %} + default-lease-time {{ subnet.lease_def }}; + {%- endif %} + {%- if subnet.lease_max %} + max-lease-time {{ subnet.lease_max }}; + {%- endif %} + {%- if subnet.lease_min %} + min-lease-time {{ subnet.lease_min }}; + {%- endif %} + {%- if subnet.dns_server %} + option dhcp6.name-servers {{ subnet.dns_server | join(', ') }}; + {%- endif %} + {%- if subnet.nis_domain %} + option dhcp6.nis-domain-name "{{ subnet.nis_domain }}"; + {%- endif %} + {%- if subnet.nis_server %} + option dhcp6.nis-servers {{ subnet.nis_server | join(', ') }}; + {%- endif %} + {%- if subnet.nisp_domain %} + option dhcp6.nisp-domain-name "{{ subnet.nisp_domain }}"; + {%- endif %} + {%- if subnet.nisp_server %} + option dhcp6.nisp-servers {{ subnet.nisp_server | join(', ') }}; + {%- endif %} + {%- if subnet.sip_address %} + option dhcp6.sip-servers-addresses {{ subnet.sip_address | join(', ') }}; + {%- endif %} + {%- if subnet.sip_hostname %} + option dhcp6.sip-servers-names {{ subnet.sip_hostname | join(', ') }}; + {%- endif %} + {%- if subnet.sntp_server %} + option dhcp6.sntp-servers {{ subnet.sntp_server | join(', ') }}; + {%- endif %} + {%- for host in subnet.static_mapping %} + {% if not host.disabled -%} + host {{ network.name }}_{{ host.name }} { + host-identifier option dhcp6.client-id "{{ host.client_identifier }}"; + fixed-address6 {{ host.ipv6_address }}; + } + {%- endif %} + {%- endfor %} + } + {%- endfor %} +} +{%- endif %} +{% endfor %} + +""" + +daemon_tmpl = """ +### Autogenerated by dhcp_server.py ### + +# sourced by /etc/init.d/isc-dhcpv6-server + +DHCPD_CONF=/etc/dhcp/dhcpd6.conf +DHCPD_PID=/var/run/dhcpd6.pid +OPTIONS="-6 -lf {{ lease_file }}" +INTERFACES="" +""" + +default_config_data = { + 'lease_file': lease_file, + 'preference': '', + 'disabled': False, + 'shared_network': [] +} + +def get_config(): + dhcpv6 = default_config_data + conf = Config() + if not conf.exists('service dhcpv6-server'): + return None + else: + conf.set_level('service dhcpv6-server') + + # Check for global disable of DHCPv6 service + if conf.exists('disable'): + dhcpv6['disabled'] = True + return dhcpv6 + + # Preference of this DHCPv6 server compared with others + if conf.exists('preference'): + dhcpv6['preference'] = conf.return_value('preference') + + # check for multiple, shared networks served with DHCPv6 addresses + if conf.exists('shared-network-name'): + for network in conf.list_nodes('shared-network-name'): + conf.set_level('service dhcpv6-server shared-network-name {0}'.format(network)) + config = { + 'name': network, + 'disabled': False, + 'subnet': [] + } + + # If disabled, the shared-network configuration becomes inactive + if conf.exists('disable'): + config['disabled'] = True + + # check for multiple subnet configurations in a shared network + if conf.exists('subnet'): + for net in conf.list_nodes('subnet'): + conf.set_level('service dhcpv6-server shared-network-name {0} subnet {1}'.format(network, net)) + subnet = { + 'network': net, + 'range6_prefix': [], + 'range6': [], + 'default_router': '', + 'dns_server': [], + 'domain_name': '', + 'domain_search': [], + 'lease_def': '', + 'lease_min': '', + 'lease_max': '', + 'nis_domain': '', + 'nis_server': [], + 'nisp_domain': '', + 'nisp_server': [], + 'sip_address': [], + 'sip_hostname': [], + 'sntp_server': [], + 'static_mapping': [] + } + + # For any subnet on which addresses will be assigned dynamically, there must be at + # least one address range statement. The range statement gives the lowest and highest + # IP addresses in a range. All IP addresses in the range should be in the subnet in + # which the range statement is declared. + if conf.exists('address-range prefix'): + for prefix in conf.list_nodes('address-range prefix'): + range = { + 'prefix': prefix, + 'temporary': False + } + + # Address range will be used for temporary addresses + if conf.exists('address-range prefix {0} temporary'.format(range['prefix'])): + range['temporary'] = True + + # Append to subnet temporary range6 list + subnet['range6_prefix'].append(range) + + if conf.exists('address-range start'): + for range in conf.list_nodes('address-range start'): + range = { + 'start': range, + 'stop': conf.return_value('address-range start {0} stop'.format(range)) + } + + # Append to subnet range6 list + subnet['range6'].append(range) + + # The domain-search option specifies a 'search list' of Domain Names to be used + # by the client to locate not-fully-qualified domain names. + if conf.exists('domain-search'): + for domain in conf.return_values('domain-search'): + subnet['domain_search'].append('"' + domain + '"') + + # IPv6 address valid lifetime + # (at the end the address is no longer usable by the client) + # (set to 30 days, the usual IPv6 default) + if conf.exists('lease-time default'): + subnet['lease_def'] = conf.return_value('lease-time default') + + # Time should be the maximum length in seconds that will be assigned to a lease. + # The only exception to this is that Dynamic BOOTP lease lengths, which are not + # specified by the client, are not limited by this maximum. + if conf.exists('lease-time maximum'): + subnet['lease_max'] = conf.return_value('lease-time maximum') + + # Time should be the minimum length in seconds that will be assigned to a lease + if conf.exists('lease-time minimum'): + subnet['lease_min'] = conf.return_value('lease-time minimum') + + # Specifies a list of Domain Name System name servers available to the client. + # Servers should be listed in order of preference. + if conf.exists('name-server'): + subnet['dns_server'] = conf.return_values('name-server') + + # Ancient NIS (Network Information Service) domain name + if conf.exists('nis-domain'): + subnet['nis_domain'] = conf.return_value('nis-domain') + + # Ancient NIS (Network Information Service) servers + if conf.exists('nis-server'): + subnet['nis_server'] = conf.return_values('nis-server') + + # Ancient NIS+ (Network Information Service) domain name + if conf.exists('nisplus-domain'): + subnet['nisp_domain'] = conf.return_value('nisplus-domain') + + # Ancient NIS+ (Network Information Service) servers + if conf.exists('nisplus-server'): + subnet['nisp_server'] = conf.return_values('nisplus-server') + + # Prefix Delegation (RFC 3633) + if conf.exists('prefix-delegation'): + print('TODO: This option is actually not implemented right now!') + + # Local SIP server that is to be used for all outbound SIP requests - IPv6 address + if conf.exists('sip-server-address'): + subnet['sip_address'] = conf.return_values('sip-server-address') + + # Local SIP server that is to be used for all outbound SIP requests - hostname + if conf.exists('sip-server-name'): + for hostname in conf.return_values('sip-server-name'): + subnet['sip_hostname'].append('"' + hostname + '"') + + # List of local SNTP servers available for the client to synchronize their clocks + if conf.exists('sntp-server'): + subnet['sntp_server'] = conf.return_values('sntp-server') + + # + # Static DHCP v6 leases + # + if conf.exists('static-mapping'): + for mapping in conf.list_nodes('static-mapping'): + conf.set_level('service dhcpv6-server shared-network-name {0} subnet {1} static-mapping {2}'.format(network, net, mapping)) + mapping = { + 'name': mapping, + 'disabled': False, + 'ipv6_address': '', + 'client_identifier': '', + } + + # This static lease is disabled + if conf.exists('disable'): + mapping['disabled'] = True + + # IPv6 address used for this DHCP client + if conf.exists('ipv6-address'): + mapping['ipv6_address'] = conf.return_value('ipv6-address') + + # This option specifies the client’s DUID identifier. DUIDs are similar but different from DHCPv4 client identifiers + if conf.exists('identifier'): + mapping['client_identifier'] = conf.return_value('identifier') + + # append static mapping configuration tu subnet list + subnet['static_mapping'].append(mapping) + + # append subnet configuration to shared network subnet list + config['subnet'].append(subnet) + + + # append shared network configuration to config dictionary + dhcpv6['shared_network'].append(config) + + return dhcpv6 + +def verify(dhcpv6): + if dhcpv6 is None: + return None + + if dhcpv6['disabled']: + return None + + # If DHCP is enabled we need one share-network + if len(dhcpv6['shared_network']) == 0: + raise ConfigError('No DHCPv6 shared networks configured.\n' \ + 'At least one DHCPv6 shared network must be configured.') + + # Inspect shared-network/subnet + subnets = [] + listen_ok = False + + for network in dhcpv6['shared_network']: + # A shared-network requires a subnet definition + if len(network['subnet']) == 0: + raise ConfigError('No DHCPv6 lease subnets configured for {0}. At least one\n' \ + 'lease subnet must be configured for each shared network.'.format(network['name'])) + + range6_start = [] + range6_stop = [] + for subnet in network['subnet']: + # Ususal range declaration with a start and stop address + for range6 in subnet['range6']: + # shorten names + start = range6['start'] + stop = range6['stop'] + + # DHCPv6 stop address is required + if start and not stop: + raise ConfigError('DHCPv6 range stop address for start {0} is not defined!'.format(start)) + + # Start address must be inside network + if not ipaddress.ip_address(start) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCPv6 range start address {0} is not in subnet {1}\n' \ + 'specified for shared network {2}!'.format(start, subnet['network'], network['name'])) + + # Stop address must be inside network + if not ipaddress.ip_address(stop) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCPv6 range stop address {0} is not in subnet {1}\n' \ + 'specified for shared network {2}!'.format(stop, subnet['network'], network['name'])) + + # Stop address must be greater or equal to start address + if not ipaddress.ip_address(stop) >= ipaddress.ip_address(start): + raise ConfigError('DHCPv6 range stop address {0} must be greater or equal\n' \ + 'to the range start address {1}!'.format(stop, start)) + + # DHCPv6 range start address must be unique - two ranges can't + # start with the same address - makes no sense + if start in range6_start: + raise ConfigError('Conflicting DHCPv6 lease range:\n' \ + 'Pool start address {0} defined multipe times!'.format(start)) + else: + range6_start.append(start) + + # DHCPv6 range stop address must be unique - two ranges can't + # end with the same address - makes no sense + if stop in range6_stop: + raise ConfigError('Conflicting DHCPv6 lease range:\n' \ + 'Pool stop address {0} defined multipe times!'.format(stop)) + else: + range6_stop.append(stop) + + # We also have prefixes that require checking + for prefix in subnet['range6_prefix']: + # If configured prefix does not match our subnet, we have to check that it's inside + if ipaddress.ip_network(prefix['prefix']) != ipaddress.ip_network(subnet['network']): + # Configured prefixes must be inside our network + if not ipaddress.ip_network(prefix['prefix']) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCPv6 prefix {0} is not in subnet {1}\n' \ + 'specified for shared network {2}!'.format(prefix['prefix'], subnet['network'], network['name'])) + + # DHCPv6 requires at least one configured address range or one static mapping + if not network['disabled']: + if vyos.validate.is_subnet_connected(subnet['network']): + listen_ok = True + + # DHCPv6 subnet must not overlap. ISC DHCP also complains about overlapping + # subnets: "Warning: subnet 2001:db8::/32 overlaps subnet 2001:db8:1::/32" + net = ipaddress.ip_network(subnet['network']) + for n in subnets: + net2 = ipaddress.ip_network(n) + if (net != net2): + if net.overlaps(net2): + raise ConfigError('DHCPv6 conflicting subnet ranges: {0} overlaps {1}'.format(net, net2)) + + if not listen_ok: + raise ConfigError('None of the DHCPv6 subnets are connected to a subnet6 on\n' \ + 'this machine. At least one subnet6 must be connected such that\n' \ + 'DHCPv6 listens on an interface!') + + + return None + +def generate(dhcpv6): + if dhcpv6 is None: + return None + + if dhcpv6['disabled']: + print('Warning: DHCPv6 server will be deactivated because it is disabled') + return None + + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(dhcpv6) + with open(config_file, 'w') as f: + f.write(config_text) + + tmpl = jinja2.Template(daemon_tmpl) + config_text = tmpl.render(dhcpv6) + with open(daemon_config_file, 'w') as f: + f.write(config_text) + + return None + +def apply(dhcpv6): + if (dhcpv6 is None) or dhcpv6['disabled']: + # DHCP server is removed in the commit + os.system('sudo systemctl stop isc-dhcpv6-server.service') + if os.path.exists(config_file): + os.unlink(config_file) + if os.path.exists(daemon_config_file): + os.unlink(daemon_config_file) + else: + # If our file holding DHCPv6 leases does yet not exist - create it + if not os.path.exists(lease_file): + os.mknod(lease_file) + + os.system('sudo systemctl restart isc-dhcpv6-server.service') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/mdns_repeater.py b/src/conf_mode/mdns_repeater.py index 474a6a5cf..cef735c0d 100755 --- a/src/conf_mode/mdns_repeater.py +++ b/src/conf_mode/mdns_repeater.py @@ -18,7 +18,7 @@ import sys import os - +import jinja2 import netifaces from vyos.config import Config @@ -26,60 +26,78 @@ from vyos import ConfigError config_file = r'/etc/default/mdns-repeater' -def get_config(): - interface_list = [] +config_tmpl = """ +### Autogenerated by mdns_repeater.py ### +DAEMON_ARGS="{{ interfaces | join(' ') }}" +""" + +default_config_data = { + 'disabled': False, + 'interfaces': [] +} +def get_config(): + mdns = default_config_data conf = Config() - conf.set_level('service mdns repeater') - if not conf.exists(''): - return interface_list + if not conf.exists('service mdns repeater'): + return None + else: + conf.set_level('service mdns repeater') - if conf.exists('interface'): - intfs_names = [] - intfs_names = conf.return_values('interface') + # Service can be disabled by user + if conf.exists('disable'): + mdns['disabled'] = True + return mdns - for name in intfs_names: - interface_list.append(name) + # Interface to repeat mDNS advertisements + if conf.exists('interface'): + mdns['interfaces'] = conf.return_values('interface') - return interface_list + return mdns def verify(mdns): - # '0' interfaces are possible, think of service deletion. Only '1' is not supported! - if len(mdns) == 1: - raise ConfigError('At least 2 interfaces must be specified but %d given!' % len(mdns)) - - # For mdns-repeater to work it is essential that the interfaces - # have an IP address assigned - for intf in mdns: - try: - netifaces.ifaddresses(intf)[netifaces.AF_INET] - except KeyError as e: - raise ConfigError('No IP address configured for interface "%s"!' % intf) + if mdns is None: + return None + + if mdns['disabled']: + return None + + # We need at least two interfaces to repeat mDNS advertisments + if len(mdns['interfaces']) < 2: + raise ConfigError('mDNS repeater requires at least 2 configured interfaces!') + + # For mdns-repeater to work it is essential that the interfaces has + # an IPv4 address assigned + for interface in mdns['interfaces']: + if netifaces.AF_INET in netifaces.ifaddresses(interface).keys(): + if len(netifaces.ifaddresses(interface)[netifaces.AF_INET]) < 1: + raise ConfigError('mDNS repeater requires an IPv6 address configured on interface %s!'.format(interface)) return None def generate(mdns): - config_header = '### Autogenerated by mdns_repeater.py ###\n' - if len(mdns) > 0: - config_args = 'DAEMON_ARGS="' + ' '.join(str(e) for e in mdns) + '"\n' - else: - config_args = 'DAEMON_ARGS=""\n' + if mdns is None: + return None + + if mdns['disabled']: + print('Warning: mDNS repeater will be deactivated because it is disabled') + return None - # write new configuration file - f = open(config_file, 'w') - f.write(config_header) - f.write(config_args) - f.close() + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(mdns) + with open(config_file, 'w') as f: + f.write(config_text) return None def apply(mdns): - if len(mdns) == 0: - cmd = "sudo systemctl stop mdns-repeater" + if (mdns is None) or mdns['disabled']: + os.system('sudo systemctl stop mdns-repeater') + if os.path.exists(config_file): + os.unlink(config_file) else: - cmd = "sudo systemctl restart mdns-repeater" + os.system('sudo systemctl restart mdns-repeater') - os.system(cmd) return None if __name__ == '__main__': diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py index 2a6088575..0abb2746a 100755 --- a/src/conf_mode/ntp.py +++ b/src/conf_mode/ntp.py @@ -36,7 +36,7 @@ config_tmpl = """ # driftfile /var/lib/ntp/ntp.drift # By default, only allow ntpd to query time sources, ignore any incoming requests -restrict default ignore +restrict default noquery nopeer notrap nomodify # Local users have unrestricted access, allowing reconfiguration via ntpdc restrict 127.0.0.1 restrict -6 ::1 @@ -154,10 +154,10 @@ def generate(ntp): def apply(ntp): if ntp is not None: - os.system('sudo /usr/sbin/invoke-rc.d ntp force-reload') + os.system('sudo systemctl restart ntp.service') else: - # NTP suuport is removed in the commit - os.system('sudo /usr/sbin/invoke-rc.d ntp stop') + # NTP support is removed in the commit + os.system('sudo systemctl stop ntp.service') os.unlink(config_file) return None diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 3b47ffc98..69952e5e2 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -21,15 +21,14 @@ import os import shutil import stat import pwd -import time import jinja2 -import ipaddress import random import binascii import re import vyos.version +import vyos.validate from vyos.config import Config from vyos import ConfigError @@ -38,6 +37,7 @@ config_file_client = r'/etc/snmp/snmp.conf' config_file_daemon = r'/etc/snmp/snmpd.conf' config_file_access = r'/usr/share/snmp/snmpd.conf' config_file_user = r'/var/lib/snmp/snmpd.conf' +config_file_init = r'/etc/default/snmpd' # SNMP OIDs used to mark auth/priv type OIDs = { @@ -59,11 +59,10 @@ clientaddr {{ trap_source }} # SNMPS template - be careful if you edit the template. access_config_tmpl = """ ### Autogenerated by snmp.py ### -{% if v3_users %} -{% for u in v3_users %} +{%- for u in v3_users %} {{ u.mode }}user {{ u.name }} -{% endfor %} -{% endif -%} +{%- endfor %} + rwuser {{ vyos_user }} """ @@ -72,20 +71,20 @@ rwuser {{ vyos_user }} user_config_tmpl = """ ### Autogenerated by snmp.py ### # user -{% if v3_users %} -{% for u in v3_users %} -{% if u.authOID == 'none' %} +{%- for u in v3_users %} +{%- if u.authOID == 'none' %} createUser {{ u.name }} -{% elif u.authPassword %} +{%- elif u.authPassword %} createUser {{ u.name }} {{ u.authProtocol | upper }} "{{ u.authPassword }}" {{ u.privProtocol | upper }} {{ u.privPassword }} -{% else %} +{%- else %} usmUser 1 3 {{ u.engineID }} "{{ u.name }}" "{{ u.name }}" NULL {{ u.authOID }} {{ u.authMasterKey }} {{ u.privOID }} {{ u.privMasterKey }} 0x -{% endif %} -{% endfor %} -{% endif %} +{%- endif %} +{%- endfor %} createUser {{ vyos_user }} MD5 "{{ vyos_user_pass }}" DES +{%- if v3_engineid %} oldEngineID {{ v3_engineid }} +{%- endif %} """ # SNMPS template - be careful if you edit the template. @@ -122,110 +121,108 @@ monitor -r 10 -e linkDownTrap "Generate linkDown" ifOperStatus == 2 ######################## # configurable section # ######################## - {% if v3_tsm_key %} [snmp] localCert {{ v3_tsm_key }} -{% endif %} +{%- endif %} # Default system description is VyOS version sysDescr VyOS {{ version }} -{% if description -%} +{% if description %} # Description SysDescr {{ description }} -{% endif %} +{%- endif %} # Listen agentaddress unix:/run/snmpd.socket{% if listen_on %}{% for li in listen_on %},{{ li }}{% endfor %}{% else %},udp:161,udp6:161{% endif %}{% if v3_tsm_key %},tlstcp:{{ v3_tsm_port }},dtlsudp::{{ v3_tsm_port }}{% endif %} # SNMP communities -{% if communities -%} -{% for c in communities %} -{% if c.network -%} -{% for network in c.network %} +{%- for c in communities %} +{%- if c.network_v4 %} +{%- for network in c.network_v4 %} {{ c.authorization }}community {{ c.name }} {{ network }} -{{ c.authorization }}community6 {{ c.name }} {{ network }} -{% endfor %} -{% else %} +{%- endfor %} +{%- else %} {{ c.authorization }}community {{ c.name }} +{%- endif %} +{%- if c.network_v6 %} +{%- for network in c.network_v6 %} +{{ c.authorization }}community6 {{ c.name }} {{ network }} +{%- endfor %} +{%- else %} {{ c.authorization }}community6 {{ c.name }} -{% endif %} -{% endfor %} -{% endif %} +{%- endif %} +{%- endfor %} -{% if contact -%} +{% if contact %} # system contact information SysContact {{ contact }} -{% endif %} +{%- endif %} -{% if location -%} +{% if location %} # system location information SysLocation {{ location }} -{% endif %} +{%- endif %} {% if smux_peers -%} # additional smux peers -{% for sp in smux_peers %} +{%- for sp in smux_peers %} smuxpeer {{ sp }} -{% endfor %} -{% endif %} +{%- endfor %} +{%- endif %} {% if trap_targets -%} # if there is a problem - tell someone! -{% for t in trap_targets %} +{%- for t in trap_targets %} trap2sink {{ t.target }}{% if t.port -%}:{{ t.port }}{% endif %} {{ t.community }} -{% endfor %} -{% endif %} +{%- endfor %} +{%- endif %} +{%- if v3_enabled %} # # SNMPv3 stuff goes here # -{% if v3_enabled %} - # views -{% if v3_views -%} -{% for v in v3_views %} -{% for oid in v.oids %} +{%- for v in v3_views %} +{%- for oid in v.oids %} view {{ v.name }} included .{{ oid.oid }} -{% endfor %} -{% endfor %} -{% endif %} +{%- endfor %} +{%- endfor %} # access # context sec.model sec.level match read write notif -{% if v3_groups -%} -{% for g in v3_groups %} -{% if g.mode == 'ro' %} -access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} none none -access {{ g.name }} "" tsm {{ g.seclevel }} exact {{ g.view }} none none -{% elif g.mode == 'rw' %} -access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} {{ g.view }} none -access {{ g.name }} "" tsm {{ g.seclevel }} exact {{ g.view }} {{ g.view }} none -{% endif %} -{% endfor -%} -{% endif %} +{%- for g in v3_groups %} +access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} {% if g.mode == 'ro' %}none{% else %}{{ g.view }}{% endif %} none +access {{ g.name }} "" tsm {{ g.seclevel }} exact {{ g.view }} {% if g.mode == 'ro' %}none{% else %}{{ g.view }}{% endif %} none +{%- endfor %} # trap-target -{% if v3_traps -%} -{% for t in v3_traps %} +{%- for t in v3_traps %} trapsess -v 3 {{ '-Ci' if t.type == 'inform' }} -e {{ t.engineID }} -u {{ t.secName }} -l {{ t.secLevel }} -a {{ t.authProtocol }} {% if t.authPassword %}-A {{ t.authPassword }}{% elif t.authMasterKey %}-3m {{ t.authMasterKey }}{% endif %} -x {{ t.privProtocol }} {% if t.privPassword %}-X {{ t.privPassword }}{% elif t.privMasterKey %}-3M {{ t.privMasterKey }}{% endif %} {{ t.ipProto }}:{{ t.ipAddr }}:{{ t.ipPort }} -{% endfor -%} -{% endif %} +{%- endfor %} # group -{% if v3_users -%} -{% for u in v3_users %} +{%- for u in v3_users %} group {{ u.group }} usm {{ u.name }} group {{ u.group }} tsm {{ u.name }} {% endfor %} -{% endif %} +{%- endif %} +""" -{% endif %} +init_config_tmpl = """ +### Autogenerated by snmp.py ### +# This file controls the activity of snmpd + +# snmpd control (yes means start daemon). +SNMPDRUN=yes +# snmpd options (use syslog, close stdin/out/err). +SNMPDOPTS='-LSed -u snmp -g snmp -p /run/snmpd.pid' """ default_config_data = { 'listen_on': [], + 'listen_address': [], 'communities': [], 'smux_peers': [], 'location' : '', @@ -271,14 +268,28 @@ def get_config(): community = { 'name': name, 'authorization': 'ro', - 'network': [] + 'network_v4': [], + 'network_v6': [] } if conf.exists('community {0} authorization'.format(name)): community['authorization'] = conf.return_value('community {0} authorization'.format(name)) + # Subnet of SNMP client(s) allowed to contact system if conf.exists('community {0} network'.format(name)): - community['network'] = conf.return_values('community {0} network'.format(name)) + for addr in conf.return_values('community {0} network'.format(name)): + if vyos.validate.is_ipv4(addr): + community['network_v4'].append(addr) + else: + community['network_v6'].append(addr) + + # IP address of SNMP client allowed to contact system + if conf.exists('community {0} client'.format(name)): + for addr in conf.return_values('community {0} client'.format(name)): + if vyos.validate.is_ipv4(addr): + community['network_v4'].append(addr) + else: + community['network_v6'].append(addr) snmp['communities'].append(community) @@ -290,21 +301,20 @@ def get_config(): if conf.exists('listen-address'): for addr in conf.list_nodes('listen-address'): - listen = '' port = '161' if conf.exists('listen-address {0} port'.format(addr)): port = conf.return_value('listen-address {0} port'.format(addr)) - if ipaddress.ip_address(addr).version == 4: - # udp:127.0.0.1:161 - listen = 'udp:' + addr + ':' + port - elif ipaddress.ip_address(addr).version == 6: - # udp6:[::1]:161 - listen = 'udp6:' + '[' + addr + ']' + ':' + port - else: - raise ConfigError('Invalid IP address version') + snmp['listen_address'].append((addr, port)) - snmp['listen_on'].append(listen) + # Always listen on localhost if an explicit address has been configured + # This is a safety measure to not end up with invalid listen addresses + # that are not configured on this system. See https://phabricator.vyos.net/T850 + if not '127.0.0.1' in conf.list_nodes('listen-address'): + snmp['listen_address'].append(('127.0.0.1', '161')) + + if not '::1' in conf.list_nodes('listen-address'): + snmp['listen_address'].append(('::1', '161')) if conf.exists('location'): snmp['location'] = conf.return_value('location') @@ -579,6 +589,24 @@ def verify(snmp): if not os.path.isfile('/config/snmp/tls/certs/' + snmp['v3_tsm_key']): raise ConfigError('TSM key must be fingerprint or filename in "/config/snmp/tls/certs/" folder') + for listen in snmp['listen_address']: + addr = listen[0] + port = listen[1] + + if vyos.validate.is_ipv4(addr): + # example: udp:127.0.0.1:161 + listen = 'udp:' + addr + ':' + port + else: + # example: udp6:[::1]:161 + listen = 'udp6:' + '[' + addr + ']' + ':' + port + + # We only wan't to configure addresses that exist on the system. + # Hint the user if they don't exist + if vyos.validate.is_addr_assigned(addr): + snmp['listen_on'].append(listen) + else: + print('WARNING: SNMP listen address {0} not configured!'.format(addr)) + if 'v3_groups' in snmp.keys(): for group in snmp['v3_groups']: # @@ -705,29 +733,35 @@ def generate(snmp): return None # Write client config file - tmpl = jinja2.Template(client_config_tmpl, trim_blocks=True) + tmpl = jinja2.Template(client_config_tmpl) config_text = tmpl.render(snmp) with open(config_file_client, 'w') as f: f.write(config_text) # Write server config file - tmpl = jinja2.Template(daemon_config_tmpl, trim_blocks=True) + tmpl = jinja2.Template(daemon_config_tmpl) config_text = tmpl.render(snmp) with open(config_file_daemon, 'w') as f: f.write(config_text) # Write access rights config file - tmpl = jinja2.Template(access_config_tmpl, trim_blocks=True) + tmpl = jinja2.Template(access_config_tmpl) config_text = tmpl.render(snmp) with open(config_file_access, 'w') as f: f.write(config_text) # Write access rights config file - tmpl = jinja2.Template(user_config_tmpl, trim_blocks=True) + tmpl = jinja2.Template(user_config_tmpl) config_text = tmpl.render(snmp) with open(config_file_user, 'w') as f: f.write(config_text) + # Write init config file + tmpl = jinja2.Template(init_config_tmpl) + config_text = tmpl.render(snmp) + with open(config_file_init, 'w') as f: + f.write(config_text) + return None def apply(snmp): @@ -761,9 +795,17 @@ def apply(snmp): # start SNMP daemon os.system("sudo systemctl restart snmpd.service") - # the passwords are not available immediately so this is a workaround - # and should be changed to polling - time.sleep(2) + # Passwords are not available immediately in the configuration file, + # after daemon startup - we wait until they have been processed by + # snmpd, which we see when a magic line appears in this file. + snmpReady = False + while not snmpReady: + with open(config_file_user, 'r') as f: + for line in f: + # Search for our magic string inside the file + if '**** DO NOT EDIT THIS FILE ****' in line: + snmpReady = True + break # Back in the Perl days the configuration was re-read and any # plaintext password inside the configuration was replaced by diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py index f1ac19473..beca7bb9a 100755 --- a/src/conf_mode/ssh.py +++ b/src/conf_mode/ssh.py @@ -236,10 +236,10 @@ def generate(ssh): def apply(ssh): if ssh is not None and 'port' in ssh.keys(): - os.system("sudo systemctl restart ssh") + os.system("sudo systemctl restart ssh.service") else: # SSH access is removed in the commit - os.system("sudo systemctl stop ssh") + os.system("sudo systemctl stop ssh.service") os.unlink(config_file) return None diff --git a/src/conf_mode/syslog.py b/src/conf_mode/syslog.py index 5dfc6f390..f652cf3d0 100755 --- a/src/conf_mode/syslog.py +++ b/src/conf_mode/syslog.py @@ -93,7 +93,7 @@ def get_config(): config_data['files'].update( { 'global' : { - 'log-file' : '/var/log/vyos-rsyslog', + 'log-file' : '/var/log/messages', 'max-size' : 262144, 'action-on-max-size' : '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog', 'selectors' : '*.notice;local7.debug', @@ -229,6 +229,18 @@ def generate(c): f.write(config_text) def verify(c): + # + # /etc/rsyslog.conf is generated somewhere and copied over the original (exists in /opt/vyatta/etc/rsyslog.conf) + # it interferes with the global logging, to make sure we are using a single base, template is enforced here + # + + if not os.path.islink('/etc/rsyslog.conf'): + os.remove('/etc/rsyslog.conf') + os.symlink('/usr/share/vyos/templates/rsyslog/rsyslog.conf', '/etc/rsyslog.conf') + + # /var/log/vyos-rsyslog were the old files, we may want to clean those up, but currently there + # is a chance that someone still needs it, so I don't automatically remove them + if c == None: return None diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py new file mode 100755 index 000000000..0984b4545 --- /dev/null +++ b/src/conf_mode/tftp_server.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import os +import stat +import pwd + +import jinja2 +import vyos.validate + +from vyos.config import Config +from vyos import ConfigError + +config_file = r'/etc/default/tftpd-hpa' + +# Please be careful if you edit the template. +config_tmpl = """ +### Autogenerated by tftp_server.py ### + +# See manual at https://linux.die.net/man/8/tftpd + +TFTP_USERNAME="tftp" +TFTP_DIRECTORY="{{ directory }}" +{% if listen_ipv4 and listen_ipv6 -%} +TFTP_ADDRESS="{% for a in listen_ipv4 -%}{{ a }}:{{ port }}{{- " --address " if not loop.last -}}{% endfor -%} {% for a in listen_ipv6 %} --address [{{ a }}]:{{ port }}{% endfor -%}" +{% elif listen_ipv4 -%} +TFTP_ADDRESS="{% for a in listen_ipv4 -%}{{ a }}:{{ port }}{{- " --address " if not loop.last -}}{% endfor %} -4" +{% elif listen_ipv6 -%} +TFTP_ADDRESS="{% for a in listen_ipv6 -%}[{{ a }}]:{{ port }}{{- " --address " if not loop.last -}}{% endfor %} -6" +{%- endif %} + +TFTP_OPTIONS="--secure {% if allow_upload %}--create --umask 000{% endif %}" + +""" + +default_config_data = { + 'directory': '', + 'allow_upload': False, + 'port': '69', + 'listen_ipv4': [], + 'listen_ipv6': [] +} + +def get_config(): + tftpd = default_config_data + conf = Config() + if not conf.exists('service tftp-server'): + return None + else: + conf.set_level('service tftp-server') + + if conf.exists('directory'): + tftpd['directory'] = conf.return_value('directory') + + if conf.exists('allow-upload'): + tftpd['allow_upload'] = True + + if conf.exists('port'): + tftpd['port'] = conf.return_value('port') + + if conf.exists('listen-address'): + for addr in conf.return_values('listen-address'): + if vyos.validate.is_ipv4(addr): + tftpd['listen_ipv4'].append(addr) + else: + tftpd['listen_ipv6'].append(addr) + + return tftpd + +def verify(tftpd): + # bail out early - looks like removal from running config + if tftpd is None: + return None + + # Configuring allowed clients without a server makes no sense + if not tftpd['directory']: + raise ConfigError('TFTP root directory must be configured!') + + if not (tftpd['listen_ipv4'] or tftpd['listen_ipv6']): + raise ConfigError('TFTP server listen address must be configured!') + + for addr in tftpd['listen_ipv4']: + # we always bind to localhost + if '127.0.0.1' not in tftpd['listen_ipv4']: + tftpd['listen_ipv4'].append('127.0.0.1') + + if not vyos.validate.is_addr_assigned(addr): + print('WARNING: TFTP server listen address {0} not configured!'.format(addr)) + + for addr in tftpd['listen_ipv6']: + # we always bind to localhost + if '::1' not in tftpd['listen_ipv6']: + tftpd['listen_ipv6'].append('::1') + + if not vyos.validate.is_addr_assigned(addr): + print('WARNING: TFTP server listen address {0} not configured!'.format(addr)) + + return None + +def generate(tftpd): + # bail out early - looks like removal from running config + if tftpd is None: + return None + + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(tftpd) + with open(config_file, 'w') as f: + f.write(config_text) + + return None + +def apply(tftpd): + if tftpd is not None: + + tftp_root = tftpd['directory'] + if not os.path.exists(tftp_root): + os.makedirs(tftp_root) + os.chmod(tftp_root, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH) + # get UNIX uid for user 'tftp' + tftp_uid = pwd.getpwnam('tftp').pw_uid + os.chown(tftp_root, tftp_uid, -1) + + os.system('sudo systemctl restart tftpd-hpa.service') + else: + # TFTP server support is removed in the commit + os.system('sudo systemctl stop tftpd-hpa.service') + os.unlink(config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index d21e3ef40..0480a886a 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -183,7 +183,7 @@ def get_config(): if not group["priority"]: group["priority"] = 100 if not group["preempt_delay"]: - group["preempt_delay"] = 5 * 60 + group["preempt_delay"] = 0 if not group["health_check_interval"]: group["health_check_interval"] = 60 if not group["health_check_count"]: @@ -273,7 +273,7 @@ def verify(data): count = len(_groups) - 1 index = 0 while (index < count): - if _groups[index]["vrid"] == _groups[index + 1]["vrid"]: + if (_groups[index]["vrid"] == _groups[index + 1]["vrid"]) and (_groups[index]["interface"] == _groups[index + 1]["interface"]): raise ConfigError("VRID {0} is used in groups {1} and {2} that both use interface {3}. Groups on the same interface must use different VRIDs".format( _groups[index]["vrid"], _groups[index]["name"], _groups[index + 1]["name"], _groups[index]["interface"])) else: diff --git a/src/conf_mode/wireguard.py b/src/conf_mode/wireguard.py index a4f876397..c6440ad81 100755 --- a/src/conf_mode/wireguard.py +++ b/src/conf_mode/wireguard.py @@ -26,101 +26,101 @@ from vyos.config import Config from vyos import ConfigError dir = r'/config/auth/wireguard' -pk = dir + '/private.key' +pk = dir + '/private.key' pub = dir + '/public.key' +psk_file = r'/tmp/psk' -### check_kmod may be removed in the future, -### just want to have everything smoothly running after reboot def check_kmod(): if not os.path.exists('/sys/module/wireguard'): - sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") + sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") if os.system('sudo modprobe wireguard') != 0: sl.syslog(sl.LOG_NOTICE, "modprobe wireguard failed") raise ConfigError("modprobe wireguard failed") def get_config(): - config_data = { - 'interfaces' : {} - } - c = Config() if not c.exists('interfaces wireguard'): return None - - c.set_level('interfaces') + + c.set_level('interfaces') intfcs = c.list_nodes('wireguard') intfcs_eff = c.list_effective_nodes('wireguard') - new_lst = list( set(intfcs) - set(intfcs_eff) ) - del_lst = list( set(intfcs_eff) - set(intfcs) ) + new_lst = list(set(intfcs) - set(intfcs_eff)) + del_lst = list(set(intfcs_eff) - set(intfcs)) - ### setting deafult and determine status of the config + config_data = { + 'interfaces' : {} + } + ### setting defaults and determine status of the config for intfc in intfcs: cnf = 'wireguard ' + intfc # default data struct - config_data['interfaces'].update ( - { - intfc : { - 'addr' : '', - 'descr' : intfc, ## snmp ifAlias - 'lport' : '', - 'status' : 'exists', - 'state' : 'enabled', - 'mtu' : 1420, - 'peer' : {} - } + config_data['interfaces'].update( + { + intfc : { + 'addr' : '', + 'descr' : intfc, ## snmp ifAlias + 'lport' : '', + 'status' : 'exists', + 'state' : 'enabled', + 'mtu' : '1420', + 'peer' : {} + } } - ) + ) + ### determine status either delete or create for i in new_lst: - config_data['interfaces'][i]['status'] = 'create' + config_data['interfaces'][i]['status'] = 'create' for i in del_lst: - config_data['interfaces'].update ( - { - i : { - 'status': 'delete' + config_data['interfaces'].update( + { + i : { + 'status': 'delete' + } } - } ) - ### based on the status, set real values + ### based on the status, setup conf values for intfc in intfcs: cnf = 'wireguard ' + intfc if config_data['interfaces'][intfc]['status'] != 'delete': - #### addresses + ### addresses if c.exists(cnf + ' address'): config_data['interfaces'][intfc]['addr'] = c.return_values(cnf + ' address') ### listen port - if c.exists(cnf + ' listen-port'): - config_data['interfaces'][intfc]['lport'] = c.return_value(cnf + ' listen-port') + if c.exists(cnf + ' port'): + config_data['interfaces'][intfc]['lport'] = c.return_value(cnf + ' port') ### description if c.exists(cnf + ' description'): config_data['interfaces'][intfc]['descr'] = c.return_value(cnf + ' description') ### mtu if c.exists(cnf + ' mtu'): config_data['interfaces'][intfc]['mtu'] = c.return_value(cnf + ' mtu') - ### peers if c.exists(cnf + ' peer'): for p in c.list_nodes(cnf + ' peer'): - config_data['interfaces'][intfc]['peer'].update ( - { - p : { - 'allowed-ips' : [], - 'endpoint' : '' + config_data['interfaces'][intfc]['peer'].update( + { + p : { + 'allowed-ips' : [], + 'endpoint' : '', + 'pubkey' : '' + } } - } ) + if c.exists(cnf + ' peer ' + p + ' pubkey'): + config_data['interfaces'][intfc]['peer'][p]['pubkey'] = c.return_value(cnf + ' peer ' + p + ' pubkey') if c.exists(cnf + ' peer ' + p + ' allowed-ips'): config_data['interfaces'][intfc]['peer'][p]['allowed-ips'] = c.return_values(cnf + ' peer ' + p + ' allowed-ips') if c.exists(cnf + ' peer ' + p + ' endpoint'): config_data['interfaces'][intfc]['peer'][p]['endpoint'] = c.return_value(cnf + ' peer ' + p + ' endpoint') + if c.exists(cnf + ' peer ' + p + ' persistent-keepalive'): + config_data['interfaces'][intfc]['peer'][p]['persistent-keepalive'] = c.return_value(cnf + ' peer ' + p + ' persistent-keepalive') + if c.exists(cnf + ' peer ' + p + ' preshared-key'): + config_data['interfaces'][intfc]['peer'][p]['psk'] = c.return_value(cnf + ' peer ' + p + ' preshared-key') - ### persistent-keepalive - if c.exists(cnf + ' peer ' + p + ' persistent-keepalive'): - config_data['interfaces'][intfc]['peer'][p]['persistent-keepalive'] = c.return_value(cnf + ' peer ' + p + ' persistent-keepalive') - - #print (config_data) return config_data def verify(c): @@ -130,34 +130,31 @@ def verify(c): for i in c['interfaces']: if c['interfaces'][i]['status'] != 'delete': if not c['interfaces'][i]['addr']: - raise ConfigError("address required for interface " + i) - if not c['interfaces'][i]['lport']: - raise ConfigError("listen-port required for interface " + i) + raise ConfigError("address required for interface " + i) if not c['interfaces'][i]['peer']: raise ConfigError("peer required on interface " + i) - else: - for p in c['interfaces'][i]['peer']: - if not c['interfaces'][i]['peer'][p]['allowed-ips']: - raise ConfigError("allowed-ips required on interface " + i + " for peer " + p) - ### eventually check allowed-ips (if it's an ip and valid CIDR or so) - ### endpoint needs to be IP:port + for p in c['interfaces'][i]['peer']: + if not c['interfaces'][i]['peer'][p]['allowed-ips']: + raise ConfigError("allowed-ips required on interface " + i + " for peer " + p) + if not c['interfaces'][i]['peer'][p]['pubkey']: + raise ConfigError("pubkey from your peer is mandatory on " + i + " for peer " + p) + def apply(c): ### no wg config left, delete all wireguard devices on the os if not c: net_devs = os.listdir('/sys/class/net/') for dev in net_devs: - buf = open('/sys/class/net/' + dev + '/uevent','r').read() + buf = open('/sys/class/net/' + dev + '/uevent', 'r').read() if re.search("DEVTYPE=wireguard", buf, re.I|re.M): - wg_intf = re.sub("INTERFACE=","", re.search("INTERFACE=.*", buf, re.I|re.M).group(0) ) + wg_intf = re.sub("INTERFACE=", "", re.search("INTERFACE=.*", buf, re.I|re.M).group(0)) sl.syslog(sl.LOG_NOTICE, "removing interface " + wg_intf) subprocess.call(['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True) return None - + ### - ## to find the diffs between old config an new config - ## so we only configure/delete what was not previously configured + ## find the diffs between effective config an new config ### c_eff = Config() c_eff.set_level('interfaces wireguard') @@ -175,88 +172,139 @@ def apply(c): subprocess.call(['ip l a dev ' + intf + ' type wireguard 2>/dev/null'], shell=True) for addr in c['interfaces'][intf]['addr']: - add_addr(intf, addr) - configure_interface(c,intf) - subprocess.call(['ip l set up dev ' + intf + ' &>/dev/null'], shell=True) + add_addr(intf, addr) + + subprocess.call(['ip l set up dev ' + intf + ' mtu ' + c['interfaces'][intf]['mtu'] + ' &>/dev/null'], shell=True) + configure_interface(c, intf) ### config updates if c['interfaces'][intf]['status'] == 'exists': ### IP address change - addr_eff = re.sub("\'", "", c_eff.return_effective_values(intf + ' address')).split() - addr_rem = list( set(addr_eff) - set(c['interfaces'][intf]['addr']) ) - addr_add = list( set(c['interfaces'][intf]['addr']) - set(addr_eff) ) + addr_eff = re.sub("\'", "", c_eff.return_effective_values(intf + ' address')).split() + addr_rem = list(set(addr_eff) - set(c['interfaces'][intf]['addr'])) + addr_add = list(set(c['interfaces'][intf]['addr']) - set(addr_eff)) - if len(addr_rem) !=0: + if len(addr_rem) != 0: for addr in addr_rem: del_addr(intf, addr) - if len(addr_add) !=0: + if len(addr_add) != 0: for addr in addr_add: add_addr(intf, addr) - ### persistent-keepalive + ## mtu update + mtu = c['interfaces'][intf]['mtu'] + if mtu != 1420: + sl.syslog(sl.LOG_NOTICE, "setting mtu to " + mtu + " on " + intf) + subprocess.call(['ip l set mtu ' + mtu + ' dev ' + intf + ' &>/dev/null'], shell=True) + + ### persistent-keepalive for p in c_eff.list_nodes(intf + ' peer'): val_eff = "" - val = "" + val = "" if c_eff.exists_effective(intf + ' peer ' + p + ' persistent-keepalive'): val_eff = c_eff.return_effective_value(intf + ' peer ' + p + ' persistent-keepalive') if 'persistent-keepalive' in c['interfaces'][intf]['peer'][p]: val = c['interfaces'][intf]['peer'][p]['persistent-keepalive'] - + ### disable keepalive if val_eff and not val: - c['interfaces'][intf]['peer'][p]['persistent-keepalive'] = 0 - + c['interfaces'][intf]['peer'][p]['persistent-keepalive'] = 0 + ### set new keepalive value if not val_eff and val: c['interfaces'][intf]['peer'][p]['persistent-keepalive'] = val ## wg command call - configure_interface(c,intf) + configure_interface(c, intf) - ### ifalias for snmp from description + ### ifalias for snmp from description descr_eff = c_eff.return_effective_value(intf + ' description') cnf_descr = c['interfaces'][intf]['descr'] if descr_eff != cnf_descr: - open('/sys/class/net/' + str(intf) + '/ifalias','w').write(str(cnf_descr)) + with open('/sys/class/net/' + str(intf) + '/ifalias', 'w') as fh: + fh.write(str(cnf_descr)) def configure_interface(c, intf): for p in c['interfaces'][intf]['peer']: - cmd = "wg set " + intf + \ - " listen-port " + c['interfaces'][intf]['lport'] + \ - " private-key " + pk + \ - " peer " + p - cmd += " allowed-ips " + ## config init for wg call + wg_config = { + 'interface' : intf, + 'port' : 0, + 'private-key' : pk, + 'pubkey' : '', + 'psk' : '/dev/null', + 'allowed-ips' : [], + 'fwmark' : 0x00, + 'endpoint' : None, + 'keepalive' : 0 + } - for ap in c['interfaces'][intf]['peer'][p]['allowed-ips']: - if ap != c['interfaces'][intf]['peer'][p]['allowed-ips'][-1]: - cmd += ap + "," - else: - cmd += ap + ## mandatory settings + wg_config['pubkey'] = c['interfaces'][intf]['peer'][p]['pubkey'] + wg_config['allowed-ips'] = c['interfaces'][intf]['peer'][p]['allowed-ips'] + + ## optional settings + # listen-port + if c['interfaces'][intf]['lport']: + wg_config['port'] = c['interfaces'][intf]['lport'] + + ## endpoint + if c['interfaces'][intf]['peer'][p]['endpoint']: + wg_config['endpoint'] = c['interfaces'][intf]['peer'][p]['endpoint'] + + ## persistent-keepalive + if 'persistent-keepalive' in c['interfaces'][intf]['peer'][p]: + wg_config['keepalive'] = c['interfaces'][intf]['peer'][p]['persistent-keepalive'] + + ## preshared-key - is only read from a file, it's called via sudo redirection doesn't work either + if 'psk' in c['interfaces'][intf]['peer'][p]: + old_umask = os.umask(0o077) + open(psk_file, 'w').write(str(c['interfaces'][intf]['peer'][p]['psk'])) + os.umask(old_umask) + wg_config['psk'] = psk_file + + ### assemble wg command + cmd = "sudo wg set " + intf + cmd += " listen-port " + str(wg_config['port']) + cmd += " private-key " + wg_config['private-key'] + cmd += " peer " + wg_config['pubkey'] + cmd += " preshared-key " + wg_config['psk'] + cmd += " allowed-ips " + for ap in wg_config['allowed-ips']: + if ap != wg_config['allowed-ips'][-1]: + cmd += ap + "," + else: + cmd += ap - ## endpoint is only required if wg runs as client - if c['interfaces'][intf]['peer'][p]['endpoint']: - cmd += " endpoint " + c['interfaces'][intf]['peer'][p]['endpoint'] + if wg_config['endpoint']: + cmd += " endpoint " + wg_config['endpoint'] - if 'persistent-keepalive' in c['interfaces'][intf]['peer'][p]: - cmd += " persistent-keepalive " + str( c['interfaces'][intf]['peer'][p]['persistent-keepalive']) + if wg_config['keepalive'] != 0: + cmd += " persistent-keepalive " + wg_config['keepalive'] + else: + cmd += " persistent-keepalive 0" - sl.syslog(sl.LOG_NOTICE, "sudo " + cmd) - subprocess.call([ 'sudo ' + cmd], shell=True) + sl.syslog(sl.LOG_NOTICE, cmd) + #print (cmd) + subprocess.call([cmd], shell=True) + """ remove psk_file """ + if os.path.exists(psk_file): + os.remove(psk_file) def add_addr(intf, addr): ret = subprocess.call(['ip a a dev ' + intf + ' ' + addr + ' &>/dev/null'], shell=True) if ret != 0: - raise ConfigError('Can\'t set IP ' + addr + ' on ' + intf ) + raise ConfigError('Can\'t set IP ' + addr + ' on ' + intf) else: sl.syslog(sl.LOG_NOTICE, "ip a a dev " + intf + " " + addr) def del_addr(intf, addr): ret = subprocess.call(['ip a d dev ' + intf + ' ' + addr + ' &>/dev/null'], shell=True) if ret != 0: - raise ConfigError('Can\'t delete IP ' + addr + ' on ' + intf ) + raise ConfigError('Can\'t delete IP ' + addr + ' on ' + intf) else: sl.syslog(sl.LOG_NOTICE, "ip a d dev " + intf + " " + addr) @@ -265,9 +313,7 @@ if __name__ == '__main__': check_kmod() c = get_config() verify(c) - #generate(c) apply(c) except ConfigError as e: print(e) sys.exit(1) - diff --git a/src/helpers/validate-value.py b/src/helpers/validate-value.py index d702739b5..36f996d38 100755 --- a/src/helpers/validate-value.py +++ b/src/helpers/validate-value.py @@ -23,7 +23,7 @@ try: except Exception as exn: if debug: print(exn) - else: + else: pass try: diff --git a/src/migration-scripts/dhcp-server/4-to-5 b/src/migration-scripts/dhcp-server/4-to-5 new file mode 100755 index 000000000..8b973d608 --- /dev/null +++ b/src/migration-scripts/dhcp-server/4-to-5 @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 + +# Removes boolean operator from: +# - "set service dhcp-server shared-network-name <xyz> subnet 172.31.0.0/24 ip-forwarding enable (true|false)" +# - "set service dhcp-server shared-network-name <xyz> authoritative (true|false)" +# - "set service dhcp-server disabled (true|false)" + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['service', 'dhcp-server']): + # Nothing to do + sys.exit(0) +else: + base = ['service', 'dhcp-server'] + # Make node "set service dhcp-server dynamic-dns-update enable (true|false)" valueless + if config.exists(base + ['dynamic-dns-update']): + bool_val = config.return_value(base + ['dynamic-dns-update', 'enable']) + + # Delete the node with the old syntax + config.delete(base + ['dynamic-dns-update']) + if str(bool_val) == 'true': + # Enable dynamic-dns-update with new syntax + config.set(base + ['dynamic-dns-update'], value=None) + + # Make node "set service dhcp-server disabled (true|false)" valueless + if config.exists(base + ['disabled']): + bool_val = config.return_value(base + ['disabled']) + + # Delete the node with the old syntax + config.delete(base + ['disabled']) + if str(bool_val) == 'true': + # Now disable DHCP server with the new syntax + config.set(base + ['disable'], value=None) + + # Make node "set service dhcp-server hostfile-update (enable|disable) valueless + if config.exists(base + ['hostfile-update']): + bool_val = config.return_value(base + ['hostfile-update']) + + # Delete the node with the old syntax incl. all subnodes + config.delete(base + ['hostfile-update']) + if str(bool_val) == 'enable': + # Enable hostfile update with new syntax + config.set(base + ['hostfile-update'], value=None) + + # Run this for every instance if 'shared-network-name' + for network in config.list_nodes(base + ['shared-network-name']): + base_network = base + ['shared-network-name', network] + # format as tag node to avoid loading problems + config.set_tag(base + ['shared-network-name']) + + # Run this for every specified 'subnet' + for subnet in config.list_nodes(base_network + ['subnet']): + base_subnet = base_network + ['subnet', subnet] + # format as tag node to avoid loading problems + config.set_tag(base_network + ['subnet']) + + # Make node "set service dhcp-server shared-network-name <xyz> subnet 172.31.0.0/24 ip-forwarding enable" valueless + if config.exists(base_subnet + ['ip-forwarding', 'enable']): + bool_val = config.return_value(base_subnet + ['ip-forwarding', 'enable']) + # Delete the node with the old syntax + config.delete(base_subnet + ['ip-forwarding']) + if str(bool_val) == 'true': + # Recreate node with new syntax + config.set(base_subnet + ['ip-forwarding'], value=None) + + # Rename node "set service dhcp-server shared-network-name <xyz> subnet 172.31.0.0/24 start <172.16.0.4> stop <172.16.0.9> + if config.exists(base_subnet + ['start']): + # This is the new "range" id for DHCP lease ranges + r_id = 0 + for range in config.list_nodes(base_subnet + ['start']): + range_start = range + range_stop = config.return_value(base_subnet + ['start', range_start, 'stop']) + + # Delete the node with the old syntax + config.delete(base_subnet + ['start', range_start]) + + # Create the node for the new syntax + # Note: range is a tag node, counter is its child, not a value + config.set(base_subnet + ['range', r_id]) + config.set(base_subnet + ['range', r_id, 'start'], value=range_start) + config.set(base_subnet + ['range', r_id, 'stop'], value=range_stop) + + # format as tag node to avoid loading problems + config.set_tag(base_subnet + ['range']) + + # increment range id for possible next range definition + r_id += 1 + + # Delete the node with the old syntax + config.delete(['service', 'dhcp-server', 'shared-network-name', network, 'subnet', subnet, 'start']) + + + # Make node "set service dhcp-server shared-network-name <xyz> authoritative" valueless + if config.exists(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative']): + bool_val = config.return_value(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative']) + # Delete the node with the old syntax + config.delete(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative']) + if str(bool_val) == 'true': + # Recreate node with new syntax + config.set(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + + sys.exit(1) diff --git a/src/migration-scripts/quagga/2-to-3 b/src/migration-scripts/quagga/2-to-3 new file mode 100755 index 000000000..99d96a0aa --- /dev/null +++ b/src/migration-scripts/quagga/2-to-3 @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys + +from vyos.configtree import ConfigTree + + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +def migrate_neighbor(config, neighbor_path, neighbor): + if config.exists(neighbor_path): + neighbors = config.list_nodes(neighbor_path) + for neighbor in neighbors: + # Move the valueless options: as-override, next-hop-self, route-reflector-client, route-server-client, + # remove-private-as + for valueless_option in ['as-override', 'nexthop-self', 'route-reflector-client', 'route-server-client', + 'remove-private-as']: + if config.exists(neighbor_path + [neighbor, valueless_option]): + config.set(neighbor_path + [neighbor] + af_path + [valueless_option]) + config.delete(neighbor_path + [neighbor, valueless_option]) + + # Move filter options: distribute-list, filter-list, prefix-list, and route-map + # They share the same syntax inside so we can group them + for filter_type in ['distribute-list', 'filter-list', 'prefix-list', 'route-map']: + if config.exists(neighbor_path + [neighbor, filter_type]): + for filter_dir in ['import', 'export']: + if config.exists(neighbor_path + [neighbor, filter_type, filter_dir]): + filter_name = config.return_value(neighbor_path + [neighbor, filter_type, filter_dir]) + config.set(neighbor_path + [neighbor] + af_path + [filter_type, filter_dir], value=filter_name) + config.delete(neighbor_path + [neighbor, filter_type]) + + # Move simple leaf node options: maximum-prefix, unsuppress-map, weight + for leaf_option in ['maximum-prefix', 'unsuppress-map', 'weight']: + if config.exists(neighbor_path + [neighbor, leaf_option]): + if config.exists(neighbor_path + [neighbor, leaf_option]): + leaf_opt_value = config.return_value(neighbor_path + [neighbor, leaf_option]) + config.set(neighbor_path + [neighbor] + af_path + [leaf_option], value=leaf_opt_value) + config.delete(neighbor_path + [neighbor, leaf_option]) + + # The rest is special cases, for better or worse + + # Move allowas-in + if config.exists(neighbor_path + [neighbor, 'allowas-in']): + if config.exists(neighbor_path + [neighbor, 'allowas-in', 'number']): + allowas_in = config.return_value(neighbor_path + [neighbor, 'allowas-in', 'number']) + config.set(neighbor_path + [neighbor] + af_path + ['allowas-in', 'number'], value=allowas_in) + config.delete(neighbor_path + [neighbor, 'allowas-in']) + + # Move attribute-unchanged options + if config.exists(neighbor_path + [neighbor, 'attribute-unchanged']): + for attr in ['as-path', 'med', 'next-hop']: + if config.exists(neighbor_path + [neighbor, 'attribute-unchanged', attr]): + config.set(neighbor_path + [neighbor] + af_path + ['attribute-unchanged', attr]) + config.delete(neighbor_path + [neighbor, 'attribute-unchanged', attr]) + config.delete(neighbor_path + [neighbor, 'attribute-unchanged']) + + # Move capability options + if config.exists(neighbor_path + [neighbor, 'capability']): + # "capability dynamic" is a peer-global option, we only migrate ORF + if config.exists(neighbor_path + [neighbor, 'capability', 'orf']): + if config.exists(neighbor_path + [neighbor, 'capability', 'orf', 'prefix-list']): + for orf in ['send', 'receive']: + if config.exists(neighbor_path + [neighbor, 'capability', 'orf', 'prefix-list', orf]): + config.set(neighbor_path + [neighbor] + af_path + ['capability', 'orf', 'prefix-list', orf]) + config.delete(neighbor_path + [neighbor, 'capability', 'orf', 'prefix-list', orf]) + config.delete(neighbor_path + [neighbor, 'capability', 'orf', 'prefix-list']) + config.delete(neighbor_path + [neighbor, 'capability', 'orf']) + + # Move default-originate + if config.exists(neighbor_path + [neighbor, 'default-originate']): + if config.exists(neighbor_path + [neighbor, 'default-originate', 'route-map']): + route_map = config.return_value(neighbor_path + [neighbor, 'default-originate', 'route-map']) + config.set(neighbor_path + [neighbor] + af_path + ['default-originate', 'route-map'], value=route_map) + else: + # Empty default-originate node is meaningful so we re-create it + config.set(neighbor_path + [neighbor] + af_path + ['default-originate']) + config.delete(neighbor_path + [neighbor, 'default-originate']) + + # Move soft-reconfiguration + if config.exists(neighbor_path + [neighbor, 'soft-reconfiguration']): + if config.exists(neighbor_path + [neighbor, 'soft-reconfiguration', 'inbound']): + config.set(neighbor_path + [neighbor] + af_path + ['soft-reconfiguration', 'inbound']) + # Empty soft-reconfiguration is meaningless, so we just remove it + config.delete(neighbor_path + [neighbor, 'soft-reconfiguration']) + + # Move disable-send-community + if config.exists(neighbor_path + [neighbor, 'disable-send-community']): + for comm_type in ['standard', 'extended']: + if config.exists(neighbor_path + [neighbor, 'disable-send-community', comm_type]): + config.set(neighbor_path + [neighbor] + af_path + ['disable-send-community', comm_type]) + config.delete(neighbor_path + [neighbor, 'disable-send-community', comm_type]) + config.delete(neighbor_path + [neighbor, 'disable-send-community']) + + +if not config.exists(['protocols', 'bgp']): + # Nothing to do + sys.exit(0) +else: + # Just to avoid writing it so many times + af_path = ['address-family', 'ipv4-unicast'] + + # Check if BGP is actually configured and obtain the ASN + asn_list = config.list_nodes(['protocols', 'bgp']) + if asn_list: + # There's always just one BGP node, if any + asn = asn_list[0] + bgp_path = ['protocols', 'bgp', asn] + else: + # There's actually no BGP, just its empty shell + sys.exit(0) + + ## Move global IPv4-specific BGP options to "address-family ipv4-unicast" + + # Move networks + network_path = ['protocols', 'bgp', asn, 'network'] + if config.exists(network_path): + config.set(bgp_path + af_path + ['network']) + config.set_tag(bgp_path + af_path + ['network']) + + networks = config.list_nodes(network_path) + for network in networks: + config.set(bgp_path + af_path + ['network', network]) + if config.exists(network_path + [network, 'route-map']): + route_map = config.return_value(network_path + [network, 'route-map']) + config.set(bgp_path + af_path + ['network', network, 'route-map'], value=route_map) + config.delete(network_path) + + # Move aggregate-address statements + aggregate_path = ['protocols', 'bgp', asn, 'aggregate-address'] + if config.exists(aggregate_path): + config.set(bgp_path + af_path + ['aggregate-address']) + config.set_tag(bgp_path + af_path + ['aggregate-address']) + + aggregates = config.list_nodes(aggregate_path) + for aggregate in aggregates: + config.set(bgp_path + af_path + ['aggregate-address', aggregate]) + if config.exists(aggregate_path + [aggregate, 'as-set']): + config.set(bgp_path + af_path + ['aggregate-address', aggregate, 'as-set']) + if config.exists(aggregate_path + [aggregate, 'summary-only']): + config.set(bgp_path + af_path + ['aggregate-address', aggregate, 'summary-only']) + config.delete(aggregate_path) + + ## Migrate neighbor options + neighbor_path = ['protocols', 'bgp', asn, 'neighbor'] + if config.exists(neighbor_path): + neighbors = config.list_nodes(neighbor_path) + for neighbor in neighbors: + migrate_neighbor(config, neighbor_path, neighbor) + + peer_group_path = ['protocols', 'bgp', asn, 'peer-group'] + if config.exists(peer_group_path): + peer_groups = config.list_nodes(peer_group_path) + for peer_group in peer_groups: + migrate_neighbor(config, peer_group_path, peer_group) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/system/8-to-9 b/src/migration-scripts/system/8-to-9 new file mode 100755 index 000000000..db3fefdea --- /dev/null +++ b/src/migration-scripts/system/8-to-9 @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +# Deletes "system package" option as it is deprecated + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['system', 'package']): + # Nothing to do + sys.exit(0) +else: + # Delete the node with the old syntax + config.delete(['system', 'package']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py new file mode 100755 index 000000000..e76fc3a14 --- /dev/null +++ b/src/op_mode/show_dhcp.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. +# + +import json +import argparse +import ipaddress + +import tabulate + +import vyos.config + +from isc_dhcp_leases import Lease, IscDhcpLeases + + +lease_file = "/config/dhcpd.leases" +pool_key = "shared-networkname" + +def in_pool(lease, pool): + if pool_key in lease.sets: + if lease.sets[pool_key] == pool: + return True + + return False + +def get_lease_data(lease): + data = {} + + # End time may not be present in backup leases + try: + data["expires"] = lease.end.strftime("%Y/%m/%d %H:%M:%S") + except: + data["expires"] = "" + + data["hardware_address"] = lease.ethernet + data["hostname"] = lease.hostname + data["ip"] = lease.ip + + try: + data["pool"] = lease.sets[pool_key] + except: + data["pool"] = "" + + return data + +def get_leases(leases, state=None, pool=None): + leases = IscDhcpLeases(lease_file).get() + + if state is not None: + leases = list(filter(lambda x: x.binding_state == 'active', leases)) + + if pool is not None: + leases = list(filter(lambda x: in_pool(x, pool), leases)) + + return list(map(get_lease_data, leases)) + +def show_leases(leases): + headers = ["IP address", "Hardware address", "Lease expiration", "Pool", "Client Name"] + + lease_list = [] + for l in leases: + lease_list.append([l["ip"], l["hardware_address"], l["expires"], l["pool"], l["hostname"]]) + + output = tabulate.tabulate(lease_list, headers) + + print(output) + +def get_pool_size(config, pool): + size = 0 + subnets = config.list_effective_nodes("service dhcp-server shared-network-name {0} subnet".format(pool)) + for s in subnets: + ranges = config.list_effective_nodes("service dhcp-server shared-network-name {0} subnet {1} range".format(pool, s)) + for r in ranges: + start = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} start".format(pool, s, r)) + stop = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} stop".format(pool, s, r)) + + size += int(ipaddress.IPv4Address(stop)) - int(ipaddress.IPv4Address(start)) + + return size + +def show_pool_stats(stats): + headers = ["Pool", "Size", "Leases", "Available", "Usage"] + output = tabulate.tabulate(stats, headers) + + print(output) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + + group = parser.add_mutually_exclusive_group() + group.add_argument("-l", "--leases", action="store_true", help="Show DHCP leases") + group.add_argument("-s", "--statistics", action="store_true", help="Show DHCP statistics") + + parser.add_argument("-e", "--expired", action="store_true", help="Show expired leases") + parser.add_argument("-p", "--pool", type=str, action="store", help="Show lease for specific pool") + parser.add_argument("-j", "--json", action="store_true", default=False, help="Product JSON output") + + args = parser.parse_args() + + if args.leases: + if args.expired: + if args.pool: + leases = get_leases(lease_file, state='free', pool=args.pool) + else: + leases = get_leases(lease_file, state='free') + else: + if args.pool: + leases = get_leases(lease_file, state='active', pool=args.pool) + else: + leases = get_leases(lease_file, state='active') + + if args.json: + print(json.dumps(leases, indent=4)) + else: + show_leases(leases) + elif args.statistics: + config = vyos.config.Config() + + pools = [] + + # Get relevant pools + if args.pool: + pools = [args.pool] + else: + pools = config.list_effective_nodes("service dhcp-server shared-network-name") + + # Get pool usage stats + stats = [] + for p in pools: + size = get_pool_size(config, p) + leases = len(get_leases(lease_file, state='active', pool=args.pool)) + use_percentage = round(leases / size) * 100 + if args.json: + pool_stats = {"pool": p, "size": size, "leases": leases, + "available": (size - leases), "percentage": use_percentage} + else: + # For tabulate + pool_stats = [p, size, leases, size - leases, "{0}%".format(use_percentage)] + stats.append(pool_stats) + + # Print stats + if args.json: + print(json.dumps(stats, indent=4)) + else: + show_pool_stats(stats) + else: + print("Use either --leases or --statistics option") diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py new file mode 100755 index 000000000..8879a45c5 --- /dev/null +++ b/src/op_mode/show_dhcpv6.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. +# + +import json +import argparse +import ipaddress + +import tabulate + +import vyos.config + +from isc_dhcp_leases import Lease, IscDhcpLeases + + +lease_file = "/config/dhcpdv6.leases" + +def get_lease_data(lease): + data = {} + + # End time may not be present in backup leases + try: + data["expires"] = lease.end.strftime("%Y/%m/%d %H:%M:%S") + except: + data["expires"] = "" + + data["duid"] = lease.host_identifier_string + data["ip"] = lease.ip + + return data + +def get_leases(leases, state=None): + leases = IscDhcpLeases(lease_file).get() + + if state is not None: + leases = list(filter(lambda x: x.binding_state == 'active', leases)) + + return list(map(get_lease_data, leases)) + +def show_leases(leases): + headers = ["IPv6 address", "Lease expiration", "DUID"] + + lease_list = [] + for l in leases: + lease_list.append([l["ip"], l["expires"], l["duid"]]) + + output = tabulate.tabulate(lease_list, headers) + + print(output) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + + group = parser.add_mutually_exclusive_group() + group.add_argument("-l", "--leases", action="store_true", help="Show DHCP leases") + group.add_argument("-s", "--statistics", action="store_true", help="Show DHCP statistics") + + parser.add_argument("-p", "--pool", type=str, action="store", help="Show lease for specific pool") + parser.add_argument("-j", "--json", action="store_true", default=False, help="Product JSON output") + + args = parser.parse_args() + + if args.leases: + leases = get_leases(lease_file, state='active') + show_leases(leases) + elif args.statistics: + print("DHCPv6 statistics option is not available") + else: + print("Invalid option") diff --git a/src/op_mode/wireguard_key.py b/src/op_mode/wireguard.py index 811cff1ca..14ee66aaf 100755 --- a/src/op_mode/wireguard_key.py +++ b/src/op_mode/wireguard.py @@ -19,18 +19,18 @@ import argparse import os import sys -import syslog as sl import subprocess +import syslog as sl from vyos import ConfigError dir = r'/config/auth/wireguard' pk = dir + '/private.key' pub = dir + '/public.key' +psk = dir + '/preshared.key' -### check_kmod may be removed in the future, -### once it's loaded automatically def check_kmod(): + """ check if kmod is loaded, if not load it """ if not os.path.exists('/sys/module/wireguard'): sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") if os.system('sudo modprobe wireguard') != 0: @@ -38,6 +38,7 @@ def check_kmod(): raise ConfigError("modprobe wireguard failed") def generate_keypair(): + """ generates a keypair which is stored in /config/auth/wireguard """ ret = subprocess.call(['wg genkey | tee ' + pk + '|wg pubkey > ' + pub], shell=True) if ret != 0: raise ConfigError("wireguard key-pair generation failed") @@ -45,18 +46,20 @@ def generate_keypair(): sl.syslog(sl.LOG_NOTICE, "new keypair wireguard key generated in " + dir) def genkey(): - ### if umask 077 makes trouble, 027 will work + """ helper function to check, regenerate the keypair """ old_umask = os.umask(0o077) if os.path.exists(pk) and os.path.exists(pub): - choice = input("You have a wireguard key-pair already, do you want to re-generate? [y/n] ") + choice = input("You already have a wireguard key-pair already, do you want to re-generate? [y/n] ") if choice == 'y' or choice == 'Y': generate_keypair() else: - os.mkdir(dir) + if not os.path.exists(dir): + os.mkdir(dir) generate_keypair() os.umask(old_umask) def showkey(key): + """ helper function to show privkey or pubkey """ if key == "pub": if os.path.exists(pub): print ( open(pub).read().strip() ) @@ -69,6 +72,10 @@ def showkey(key): else: print("no private key found") +def genpsk(): + """ generates a preshared key and shows it on stdout, it's stroed only in the config """ + subprocess.call(['wg genpsk'], shell=True) + if __name__ == '__main__': check_kmod() @@ -76,6 +83,7 @@ if __name__ == '__main__': parser.add_argument('--genkey', action="store_true", help='generate key-pair') parser.add_argument('--showpub', action="store_true", help='shows public key') parser.add_argument('--showpriv', action="store_true", help='shows private key') + parser.add_argument('--genpsk', action="store_true", help='generates preshared-key') args = parser.parse_args() try: @@ -85,6 +93,8 @@ if __name__ == '__main__': showkey("pub") if args.showpriv: showkey("pk") + if args.genpsk: + genpsk() except ConfigError as e: print(e) diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh new file mode 100755 index 000000000..d671bffd6 --- /dev/null +++ b/src/system/on-dhcp-event.sh @@ -0,0 +1,98 @@ +#!/bin/bash + +# This script came from ubnt.com forum user "bradd" in the following post +# http://community.ubnt.com/t5/EdgeMAX/Automatic-DNS-resolution-of-DHCP-client-names/td-p/651311 +# It has been modified by Ubiquiti to update the /etc/host file +# instead of adding to the CLI. +# Thanks to forum user "itsmarcos" for bug fix & improvements +# Thanks to forum user "ruudboon" for multiple domain fix +# Thanks to forum user "chibby85" for expire patch and static-mapping + +if [ $# -lt 5 ]; then + echo Invalid args + logger -s -t on-dhcp-event "Invalid args \"$@\"" + exit 1 +fi + +action=$1 +client_name=$2 +client_ip=$3 +client_mac=$4 +domain=$5 +file=/etc/hosts +changes=0 + +if [ "$domain" == "..YYZ!" ]; then + client_fqdn_name=$client_name + client_search_expr=$client_name +else + client_fqdn_name=$client_name.$domain + client_search_expr="$client_name\\.$domain" +fi + +case "$action" in + commit) # add mapping for new lease + echo "- new lease event, setting static mapping for host "\ + "$client_fqdn_name (MAC=$client_mac, IP=$client_ip)" + # + # grep fails miserably with \t in the search expression. + # In the following line one <Ctrl-V> <TAB> is used after $client_search_expr + # followed by a single space + grep -q " $client_search_expr #on-dhcp-event " $file + if [ $? == 0 ]; then + echo pattern found, removing + wc1=`cat $file | wc -l` + sudo sed -i "/ $client_search_expr\t #on-dhcp-event /d" $file + wc2=`cat $file | wc -l` + if [ "$wc1" -eq "$wc2" ]; then + echo No change + fi + else + echo pattern NOT found + fi + + # check if hostname already exists (e.g. a static host mapping) + # if so don't overwrite + grep -q " $client_search_expr " $file + if [ $? == 0 ]; then + echo host $client_fqdn_name already exists, exiting + exit 1 + fi + + line="$client_ip\t $client_fqdn_name\t #on-dhcp-event $client_mac" + sudo sh -c "echo -e '$line' >> $file" + ((changes++)) + echo Entry was added + ;; + + release) # delete mapping for released address + echo "- lease release event, deleting static mapping for host $client_fqdn_name" + wc1=`cat $file | wc -l` + sudo sed -i "/ $client_search_expr\t #on-dhcp-event /d" $file + wc2=`cat $file | wc -l` + if [ "$wc1" -eq "$wc2" ]; then + echo No change + else + echo Entry was removed + ((changes++)) + fi + ;; + + *) + logger -s -t on-dhcp-event "Invalid command \"$1\"" + exit 1; + ;; +esac + +if [ $changes -gt 0 ]; then + echo Success + pid=`pgrep pdns_recursor` + if [ -n "$pid" ]; then + sudo rec_control reload-zones + fi +else + echo No changes made +fi +exit 0 + + |