diff options
72 files changed, 6273 insertions, 1654 deletions
diff --git a/.gitignore b/.gitignore index b42512134..2d84a4209 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,9 @@ debian/debhelper-build-stamp # Sonar Cloud .scannerwork /.vs + +# SlickEdit +*.vpj +*.vpw +*.vpwhist +*.vtg @@ -12,6 +12,8 @@ interface_definitions: rm -f $(TMPL_DIR)/firewall/node.def rm -f $(TMPL_DIR)/interfaces/node.def rm -f $(TMPL_DIR)/interfaces/bridge/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/vxlan/node.tag/ip/node.def rm -f $(TMPL_DIR)/protocols/node.def rm -f $(TMPL_DIR)/protocols/static/node.def rm -f $(TMPL_DIR)/system/node.def @@ -32,12 +34,15 @@ op_mode_definitions: rm -f $(OP_TMPL_DIR)/show/node.def rm -f $(OP_TMPL_DIR)/show/interfaces/node.def rm -f $(OP_TMPL_DIR)/show/ip/node.def - rm -f $(OP_TMPL_DIR)/reset/node.def + rm -f $(OP_TMPL_DIR)/show/ip/route/node.def + rm -f $(OP_TMPL_DIR)/show/ipv6/node.def + rm -f $(OP_TMPL_DIR)/show/ipv6/route/node.def rm -f $(OP_TMPL_DIR)/restart/node.def rm -f $(OP_TMPL_DIR)/monitor/node.def rm -f $(OP_TMPL_DIR)/generate/node.def rm -f $(OP_TMPL_DIR)/show/vpn/node.def rm -f $(OP_TMPL_DIR)/show/system/node.def + rm -f $(OP_TMPL_DIR)/delete/node.def .PHONY: all all: clean interface_definitions op_mode_definitions diff --git a/debian/control b/debian/control index a65d0158e..dce463157 100644 --- a/debian/control +++ b/debian/control @@ -28,6 +28,7 @@ Depends: python3, python3-hurry.filesize, python3-vici (>= 5.7.2), python3-bottle, + python3-zmq, ipaddrcheck, tcpdump, tshark, @@ -61,6 +62,10 @@ Depends: python3, openvpn, openvpn-auth-ldap, openvpn-auth-radius, + mtr-tiny, + telnet, + traceroute, + ssl-cert, nginx-light, ${shlibs:Depends}, ${misc:Depends} Description: VyOS configuration scripts and data diff --git a/interface-definitions/dns-forwarding.xml b/interface-definitions/dns-forwarding.xml index 56820608c..a88c174e3 100644 --- a/interface-definitions/dns-forwarding.xml +++ b/interface-definitions/dns-forwarding.xml @@ -97,6 +97,23 @@ <valueless/> </properties> </leafNode> + <leafNode name="allow-from"> + <properties> + <help>Networks allowed to query this server</help> + <valueHelp> + <format>ipv4net</format> + <description>IP address and prefix length</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ip-prefix"/> + </constraint> + </properties> + </leafNode> <leafNode name="listen-address"> <properties> <help>Addresses to listen for DNS queries [REQUIRED]</help> @@ -115,15 +132,6 @@ </constraint> </properties> </leafNode> - <leafNode name="listen-on"> - <properties> - <help>Interface to listen for DNS queries [DEPRECATED]</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - <multi/> - </properties> - </leafNode> <leafNode name="negative-ttl"> <properties> <help>Maximum amount of time negative entries are cached</help> diff --git a/interface-definitions/https.xml b/interface-definitions/https.xml index 13d5c43ea..2fb3bf082 100644 --- a/interface-definitions/https.xml +++ b/interface-definitions/https.xml @@ -9,7 +9,7 @@ <priority>1001</priority> </properties> <children> - <leafNode name="listen-address"> + <tagNode name="listen-address"> <properties> <help>Addresses to listen for HTTPS requests</help> <valueHelp> @@ -20,13 +20,25 @@ <format>ipv6</format> <description>HTTPS IPv6 address</description> </valueHelp> - <multi/> + <valueHelp> + <format>'*'</format> + <description>any</description> + </valueHelp> <constraint> <validator name="ipv4-address"/> <validator name="ipv6-address"/> + <regex>^\\*$</regex> </constraint> </properties> - </leafNode> + <children> + <leafNode name="server-name"> + <properties> + <help>Server names: exact, wildcard, regex, or '_' (any)</help> + <multi/> + </properties> + </leafNode> + </children> + </tagNode> <node name="certificates"> <properties> <help>TLS certificates</help> diff --git a/interface-definitions/interfaces-bonding.xml b/interface-definitions/interfaces-bonding.xml new file mode 100644 index 000000000..88dbab6ab --- /dev/null +++ b/interface-definitions/interfaces-bonding.xml @@ -0,0 +1,673 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="bonding" owner="${vyos_conf_scripts_dir}/interface-bonding.py"> + <properties> + <help>Bonding interface name</help> + <priority>315</priority> + <constraint> + <regex>bond[0-9]+$</regex> + </constraint> + <constraintErrorMessage>Bonding interface must be named bondN</constraintErrorMessage> + <valueHelp> + <format>bondN</format> + <description>Bonding interface name</description> + </valueHelp> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IP address</help> + <completionHelp> + <list>dhcp dhcpv6</list> + </completionHelp> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <valueHelp> + <format>dhcp</format> + <description>Dynamic Host Configuration Protocol</description> + </valueHelp> + <valueHelp> + <format>dhcpv6</format> + <description>Dynamic Host Configuration Protocol for IPv6</description> + </valueHelp> + <constraint> + <validator name="ip-cidr"/> + <regex>(dhcp|dhcpv6)</regex> + </constraint> + <multi/> + </properties> + </leafNode> + <node name="arp-monitor"> + <properties> + <help>ARP link monitoring parameters</help> + </properties> + <children> + <leafNode name="interval"> + <properties> + <help>ARP link monitoring interval</help> + <valueHelp> + <format>0-4294967295</format> + <description>Specifies the ARP link monitoring frequency in milliseconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="target"> + <properties> + <help>IP address used for ARP monitoring</help> + <valueHelp> + <format>ipv4</format> + <description>Network Time Protocol (NTP) IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="description"> + <properties> + <help>Interface description</help> + <constraint> + <regex>^.{1,256}$</regex> + </constraint> + <constraintErrorMessage>Interface description too long (limit 256 characters)</constraintErrorMessage> + </properties> + </leafNode> + <node name="dhcp-options"> + <properties> + <help>DHCP options</help> + </properties> + <children> + <leafNode name="client-id"> + <properties> + <help>DHCP client identifier</help> + </properties> + </leafNode> + <leafNode name="host-name"> + <properties> + <help>DHCP client host name (overrides the system host name)</help> + </properties> + </leafNode> + </children> + </node> + <node name="dhcpv6-options"> + <properties> + <help>DHCPv6 options</help> + <priority>319</priority> + </properties> + <children> + <leafNode name="parameters-only"> + <properties> + <help>Acquire only config parameters, no address</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="temporary"> + <properties> + <help>IPv6 "temporary" address</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="disable-link-detect"> + <properties> + <help>Ignore link state changes</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="disable"> + <properties> + <help>Disable this bridge interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="hash-policy"> + <properties> + <help>Bonding transmit hash policy</help> + <completionHelp> + <list>layer2 layer2+3 layer3+4</list> + </completionHelp> + <valueHelp> + <format>layer2</format> + <description>use MAC addresses to generate the hash (802.3ad, default)</description> + </valueHelp> + <valueHelp> + <format>layer2+3</format> + <description>combine MAC address and IP address to make hash</description> + </valueHelp> + <valueHelp> + <format>layer3+4</format> + <description>combine IP address and port to make hash</description> + </valueHelp> + <constraint> + <regex>(layer2\\+3|layer3\\+4|layer2)</regex> + </constraint> + <constraintErrorMessage>hash-policy must be layer2 layer2+3 or layer3+4</constraintErrorMessage> + </properties> + </leafNode> + <node name="ip"> + <children> + <leafNode name="arp-cache-timeout"> + <properties> + <help>ARP cache entry timeout in seconds</help> + <valueHelp> + <format>1-86400</format> + <description>ARP cache entry timout in seconds (default 30)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-86400"/> + </constraint> + <constraintErrorMessage>ARP cache entry timeout must be between 1 and 86400 seconds</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="enable-proxy-arp"> + <properties> + <help>Enable proxy-arp on this interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="proxy-arp-pvlan"> + <properties> + <help>Enable private VLAN proxy ARP on this interface</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="mac"> + <properties> + <help>Media Access Control (MAC) address</help> + <valueHelp> + <format>h:h:h:h:h:h</format> + <description>Hardware (MAC) address</description> + </valueHelp> + <constraint> + <validator name="mac-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="mode"> + <properties> + <help>Bonding mode</help> + <completionHelp> + <list>802.3ad active-backup broadcast round-robin transmit-load-balance adaptive-load-balance xor-hash</list> + </completionHelp> + <valueHelp> + <format>802.3ad</format> + <description>IEEE 802.3ad Dynamic link aggregation (Default)</description> + </valueHelp> + <valueHelp> + <format>active-backup</format> + <description>Fault tolerant: only one slave in the bond is active</description> + </valueHelp> + <valueHelp> + <format>broadcast</format> + <description>Fault tolerant: transmits everything on all slave interfaces</description> + </valueHelp> + <valueHelp> + <format>round-robin</format> + <description>Load balance: transmit packets in sequential order</description> + </valueHelp> + <valueHelp> + <format>transmit-load-balance</format> + <description>Load balance: adapts based on transmit load and speed</description> + </valueHelp> + <valueHelp> + <format>adaptive-load-balance</format> + <description>Load balance: adapts based on transmit and receive plus ARP</description> + </valueHelp> + <valueHelp> + <format>xor-hash</format> + <description>Distribute based on MAC address</description> + </valueHelp> + <constraint> + <regex>(802.3ad|active-backup|broadcast|round-robin|transmit-load-balance|adaptive-load-balance|xor-hash)</regex> + </constraint> + <constraintErrorMessage>mode must be 802.3ad, active-backup, broadcast, round-robin, transmit-load-balance, adaptive-load-balance, or xor</constraintErrorMessage> + </properties> + </leafNode> + <node name="member"> + <properties> + <help>Bridge member interfaces</help> + </properties> + <children> + <leafNode name="interface"> + <properties> + <help>Member interface name</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --bondable</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="mtu"> + <properties> + <help>Maximum Transmission Unit (MTU)</help> + <valueHelp> + <format>68-9000</format> + <description>Maximum Transmission Unit</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 68-9000"/> + </constraint> + <constraintErrorMessage>MTU must be between 68 and 9000</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="primary"> + <properties> + <help>Primary device interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --bondable</script> + </completionHelp> + </properties> + </leafNode> + <tagNode name="vif-s"> + <properties> + <help>QinQ TAG-S Virtual Local Area Network (VLAN) ID</help> + <constraint> + <validator name="numeric" argument="--range 0-4094"/> + </constraint> + <constraintErrorMessage>VLAN ID must be between 0 and 4094</constraintErrorMessage> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IP address</help> + <completionHelp> + <list>dhcp dhcpv6</list> + </completionHelp> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <valueHelp> + <format>dhcp</format> + <description>Dynamic Host Configuration Protocol</description> + </valueHelp> + <valueHelp> + <format>dhcpv6</format> + <description>Dynamic Host Configuration Protocol for IPv6</description> + </valueHelp> + <constraint> + <validator name="ip-cidr"/> + <regex>(dhcp|dhcpv6)</regex> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="description"> + <properties> + <help>Interface description</help> + <constraint> + <regex>^.{1,256}$</regex> + </constraint> + <constraintErrorMessage>Interface description too long (limit 256 characters)</constraintErrorMessage> + </properties> + </leafNode> + <node name="dhcp-options"> + <properties> + <help>DHCP options</help> + </properties> + <children> + <leafNode name="client-id"> + <properties> + <help>DHCP client identifier</help> + </properties> + </leafNode> + <leafNode name="host-name"> + <properties> + <help>DHCP client host name (overrides the system host name)</help> + </properties> + </leafNode> + </children> + </node> + <node name="dhcpv6-options"> + <properties> + <help>DHCPv6 options</help> + <priority>319</priority> + </properties> + <children> + <leafNode name="parameters-only"> + <properties> + <help>Acquire only config parameters, no address</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="temporary"> + <properties> + <help>IPv6 "temporary" address</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="disable-link-detect"> + <properties> + <help>Ignore link state changes</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="disable"> + <properties> + <help>Disable this bridge interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ethertype"> + <properties> + <help>Set Ethertype</help> + <completionHelp> + <list>0x88A8 0x8100</list> + </completionHelp> + <valueHelp> + <format>0x88A8</format> + <description>802.1ad</description> + </valueHelp> + <valueHelp> + <format>0x8100</format> + <description>802.1q</description> + </valueHelp> + <constraint> + <regex>(0x88A8|0x8100)</regex> + </constraint> + <constraintErrorMessage>Ethertype must be 0x88A8 or 0x8100</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="mac"> + <properties> + <help>Media Access Control (MAC) address</help> + <valueHelp> + <format>h:h:h:h:h:h</format> + <description>Hardware (MAC) address</description> + </valueHelp> + <constraint> + <validator name="mac-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="mtu"> + <properties> + <help>Maximum Transmission Unit (MTU)</help> + <valueHelp> + <format>68-9000</format> + <description>Maximum Transmission Unit</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 68-9000"/> + </constraint> + <constraintErrorMessage>MTU must be between 68 and 9000</constraintErrorMessage> + </properties> + </leafNode> + <tagNode name="vif-c"> + <properties> + <help>QinQ TAG-C Virtual Local Area Network (VLAN) ID</help> + <constraint> + <validator name="numeric" argument="--range 0-4094"/> + </constraint> + <constraintErrorMessage>VLAN ID must be between 0 and 4094</constraintErrorMessage> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IP address</help> + <completionHelp> + <list>dhcp dhcpv6</list> + </completionHelp> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <valueHelp> + <format>dhcp</format> + <description>Dynamic Host Configuration Protocol</description> + </valueHelp> + <valueHelp> + <format>dhcpv6</format> + <description>Dynamic Host Configuration Protocol for IPv6</description> + </valueHelp> + <constraint> + <validator name="ip-cidr"/> + <regex>(dhcp|dhcpv6)</regex> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="description"> + <properties> + <help>Interface description</help> + <constraint> + <regex>^.{1,256}$</regex> + </constraint> + <constraintErrorMessage>Interface description too long (limit 256 characters)</constraintErrorMessage> + </properties> + </leafNode> + <node name="dhcp-options"> + <properties> + <help>DHCP options</help> + </properties> + <children> + <leafNode name="client-id"> + <properties> + <help>DHCP client identifier</help> + </properties> + </leafNode> + <leafNode name="host-name"> + <properties> + <help>DHCP client host name (overrides the system host name)</help> + </properties> + </leafNode> + </children> + </node> + <node name="dhcpv6-options"> + <properties> + <help>DHCPv6 options</help> + <priority>319</priority> + </properties> + <children> + <leafNode name="parameters-only"> + <properties> + <help>Acquire only config parameters, no address</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="temporary"> + <properties> + <help>IPv6 "temporary" address</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="disable-link-detect"> + <properties> + <help>Ignore link state changes</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="disable"> + <properties> + <help>Disable this bridge interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="mac"> + <properties> + <help>Media Access Control (MAC) address</help> + <valueHelp> + <format>h:h:h:h:h:h</format> + <description>Hardware (MAC) address</description> + </valueHelp> + <constraint> + <validator name="mac-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="mtu"> + <properties> + <help>Maximum Transmission Unit (MTU)</help> + <valueHelp> + <format>68-9000</format> + <description>Maximum Transmission Unit</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 68-9000"/> + </constraint> + <constraintErrorMessage>MTU must be between 68 and 9000</constraintErrorMessage> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + <tagNode name="vif"> + <properties> + <help>Virtual Local Area Network (VLAN) ID</help> + <constraint> + <validator name="numeric" argument="--range 0-4094"/> + </constraint> + <constraintErrorMessage>VLAN ID must be between 0 and 4094</constraintErrorMessage> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IP address</help> + <completionHelp> + <list>dhcp dhcpv6</list> + </completionHelp> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <valueHelp> + <format>dhcp</format> + <description>Dynamic Host Configuration Protocol</description> + </valueHelp> + <valueHelp> + <format>dhcpv6</format> + <description>Dynamic Host Configuration Protocol for IPv6</description> + </valueHelp> + <constraint> + <validator name="ip-cidr"/> + <regex>(dhcp|dhcpv6)</regex> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="description"> + <properties> + <help>Interface description</help> + <constraint> + <regex>^.{1,256}$</regex> + </constraint> + <constraintErrorMessage>Interface description too long (limit 256 characters)</constraintErrorMessage> + </properties> + </leafNode> + <node name="dhcp-options"> + <properties> + <help>DHCP options</help> + </properties> + <children> + <leafNode name="client-id"> + <properties> + <help>DHCP client identifier</help> + </properties> + </leafNode> + <leafNode name="host-name"> + <properties> + <help>DHCP client host name (overrides the system host name)</help> + </properties> + </leafNode> + </children> + </node> + <node name="dhcpv6-options"> + <properties> + <help>DHCPv6 options</help> + <priority>319</priority> + </properties> + <children> + <leafNode name="parameters-only"> + <properties> + <help>Acquire only config parameters, no address</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="temporary"> + <properties> + <help>IPv6 "temporary" address</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="disable-link-detect"> + <properties> + <help>Ignore link state changes</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="disable"> + <properties> + <help>Disable this bridge interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="mac"> + <properties> + <help>Media Access Control (MAC) address</help> + <valueHelp> + <format>h:h:h:h:h:h</format> + <description>Hardware (MAC) address</description> + </valueHelp> + <constraint> + <validator name="mac-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="mtu"> + <properties> + <help>Maximum Transmission Unit (MTU)</help> + <valueHelp> + <format>68-9000</format> + <description>Maximum Transmission Unit</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 68-9000"/> + </constraint> + <constraintErrorMessage>MTU must be between 68 and 9000</constraintErrorMessage> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-bridge.xml b/interface-definitions/interfaces-bridge.xml index adb525a46..4b82972dc 100644 --- a/interface-definitions/interfaces-bridge.xml +++ b/interface-definitions/interfaces-bridge.xml @@ -47,18 +47,17 @@ </leafNode> <leafNode name="aging"> <properties> - <help>Interval addresses are retained</help> + <help>MAC address aging interval</help> <valueHelp> <format>0</format> - <description>Disable retaining address in bridge (always flood)</description> + <description>Disable MAC address learning (always flood)</description> </valueHelp> <valueHelp> <format>10-1000000</format> - <description>Address aging time for bridge seconds (default 300)</description> + <description>MAC address aging time in seconds (default: 300)</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 0-0"/> - <validator name="numeric" argument="--range 10-1000000"/> + <validator name="numeric" argument="--range 0-0 --range 10-1000000"/> </constraint> </properties> </leafNode> @@ -117,6 +116,7 @@ <leafNode name="disable"> <properties> <help>Disable this bridge interface</help> + <valueless/> </properties> </leafNode> <leafNode name="forwarding-delay"> @@ -170,7 +170,7 @@ <constraint> <validator name="numeric" argument="--range 1-86400"/> </constraint> - <constraintErrorMessage>Bridge max aging value must be between 6 and 86400 seconds</constraintErrorMessage> + <constraintErrorMessage>ARP cache entry timeout must be between 1 and 86400 seconds</constraintErrorMessage> </properties> </leafNode> </children> diff --git a/interface-definitions/interfaces-dummy.xml b/interface-definitions/interfaces-dummy.xml new file mode 100644 index 000000000..c9860fe3b --- /dev/null +++ b/interface-definitions/interfaces-dummy.xml @@ -0,0 +1,55 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="dummy" owner="${vyos_conf_scripts_dir}/interface-dummy.py"> + <properties> + <help>Dummy interface name</help> + <priority>300</priority> + <constraint> + <regex>dum[0-9]+$</regex> + </constraint> + <constraintErrorMessage>Dummy interface must be named dumN</constraintErrorMessage> + <valueHelp> + <format>dumN</format> + <description>Dummy interface name</description> + </valueHelp> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IP address</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ip-cidr"/> + </constraint> + </properties> + </leafNode> + <leafNode name="description"> + <properties> + <help>Interface description</help> + <constraint> + <regex>^.{1,256}$</regex> + </constraint> + <constraintErrorMessage>Interface description too long (limit 256 characters)</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="disable"> + <properties> + <help>Disable interface</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-loopback.xml b/interface-definitions/interfaces-loopback.xml new file mode 100644 index 000000000..267731b1c --- /dev/null +++ b/interface-definitions/interfaces-loopback.xml @@ -0,0 +1,46 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="loopback" owner="${vyos_conf_scripts_dir}/interface-loopback.py"> + <properties> + <help>Loopback interface</help> + <priority>300</priority> + <constraint> + <regex>lo$</regex> + </constraint> + <constraintErrorMessage>Loopback interface must be named lo</constraintErrorMessage> + <valueHelp> + <format>lo</format> + <description>Loopback interface</description> + </valueHelp> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IP address</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <multi/> + </properties> + </leafNode> + <leafNode name="description"> + <properties> + <help>Interface description</help> + <constraint> + <regex>^.{1,256}$</regex> + </constraint> + <constraintErrorMessage>Interface description too long (limit 256 characters)</constraintErrorMessage> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-openvpn.xml b/interface-definitions/interfaces-openvpn.xml index bb5c5a965..d282a8773 100644 --- a/interface-definitions/interfaces-openvpn.xml +++ b/interface-definitions/interfaces-openvpn.xml @@ -42,7 +42,7 @@ <properties> <help>Interface to a bridge-group</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py -t bridge</script> + <script>${vyos_completion_dir}/list_interfaces.py --type bridge</script> </completionHelp> </properties> </leafNode> diff --git a/interface-definitions/interfaces-vxlan.xml b/interface-definitions/interfaces-vxlan.xml new file mode 100644 index 000000000..b06c2860c --- /dev/null +++ b/interface-definitions/interfaces-vxlan.xml @@ -0,0 +1,151 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="vxlan" owner="${vyos_conf_scripts_dir}/interface-vxlan.py"> + <properties> + <help>Virtual extensible LAN interface (VXLAN)</help> + <priority>460</priority> + <constraint> + <regex>vxlan[0-9]+$</regex> + </constraint> + <constraintErrorMessage>VXLAN interface must be named vxlanN</constraintErrorMessage> + <valueHelp> + <format>vxlanN</format> + <description>VXLAN interface name</description> + </valueHelp> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IP address</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ip-cidr"/> + </constraint> + </properties> + </leafNode> + <leafNode name="description"> + <properties> + <help>Interface description</help> + <constraint> + <regex>^.{1,256}$</regex> + </constraint> + <constraintErrorMessage>Interface description too long (limit 256 characters)</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="disable"> + <properties> + <help>Disable interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="group"> + <properties> + <help>Multicast group address for VXLAN interface</help> + <valueHelp> + <format>ipv4</format> + <description>Multicast group address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <node name="ip"> + <children> + <leafNode name="arp-cache-timeout"> + <properties> + <help>ARP cache entry timeout in seconds</help> + <valueHelp> + <format>1-86400</format> + <description>ARP cache entry timout in seconds (default 30)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-86400"/> + </constraint> + <constraintErrorMessage>ARP cache entry timeout must be between 1 and 86400 seconds</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="enable-proxy-arp"> + <properties> + <help>Enable proxy-arp on this interface</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="link"> + <properties> + <help>Underlay device of VXLAN interface</help> + <valueHelp> + <format>interface</format> + <description>Interface used for VXLAN underlay</description> + </valueHelp> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + </leafNode> + <leafNode name="mtu"> + <properties> + <help>Maximum Transmission Unit (MTU)</help> + <valueHelp> + <format>1450-9000</format> + <description>Maximum Transmission Unit</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1450-9000"/> + </constraint> + <constraintErrorMessage>MTU must be between 1450 and 9000</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="remote"> + <properties> + <help>Remote address of VXLAN tunnel</help> + <valueHelp> + <format>ipv4</format> + <description>Remote address of VXLAN tunnel</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Destination port of VXLAN tunnel (default: 8472)</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="vni"> + <properties> + <help>Virtual Network Identifier</help> + <valueHelp> + <format>0-16777214</format> + <description>VXLAN virtual network identifier</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-16777214"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-wireguard.xml b/interface-definitions/interfaces-wireguard.xml index 6e2622018..f2a7cc316 100644 --- a/interface-definitions/interfaces-wireguard.xml +++ b/interface-definitions/interfaces-wireguard.xml @@ -77,6 +77,14 @@ </constraint> </properties> </leafNode> + <leafNode name="private-key"> + <properties> + <help>Private key to use on that interface</help> + <completionHelp> + <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script> + </completionHelp> + </properties> + </leafNode> <tagNode name="peer"> <properties> <help>peer alias</help> diff --git a/op-mode-definitions/bandwidth-test.xml b/op-mode-definitions/bandwidth-test.xml new file mode 100644 index 000000000..d1e459b17 --- /dev/null +++ b/op-mode-definitions/bandwidth-test.xml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="monitor"> + <children> + <node name="bandwidth-test"> + <properties> + <help>Initiate or wait for bandwidth test</help> + </properties> + <children> + <leafNode name="accept"> + <properties> + <help>Wait for bandwidth test connections (port TCP/5001)</help> + </properties> + <command>iperf -s</command> + </leafNode> + <tagNode name="initiate"> + <properties> + <help>Initiate a bandwidth test to specified host (port TCP/5001)</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>iperf -c $4</command> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/disks.xml b/op-mode-definitions/disks.xml new file mode 100644 index 000000000..fb39c4f3c --- /dev/null +++ b/op-mode-definitions/disks.xml @@ -0,0 +1,50 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="format"> + <properties> + <help>Format a device</help> + </properties> + <children> + <tagNode name="disk"> + <properties> + <help>Format a disk drive</help> + <completionHelp> + <script>${vyos_completion_dir}/list_disks.py</script> + </completionHelp> + </properties> + <children> + <tagNode name="like"> + <properties> + <help>Format this disk the same as another disk</help> + <completionHelp> + <script>${vyos_completion_dir}/list_disks.py --exclude ${COMP_WORDS[2]}</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/format_disk.py --target $3 --proto $5</command> + </tagNode> + </children> + </tagNode> + </children> + </node> + + <node name="show"> + <children> + <tagNode name="disk"> + <properties> + <help>Show status of disk device</help> + <completionHelp> + <script>${vyos_completion_dir}/list_disks.py</script> + </completionHelp> + </properties> + <children> + <leafNode name="format"> + <properties> + <help>Show disk drive formatting</help> + </properties> + <command>${vyos_op_scripts_dir}/show_disk_format.sh $3</command> + </leafNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/dns-forwarding.xml b/op-mode-definitions/dns-forwarding.xml index ac141174f..785a05e9c 100644 --- a/op-mode-definitions/dns-forwarding.xml +++ b/op-mode-definitions/dns-forwarding.xml @@ -42,6 +42,9 @@ </children> </node> <node name="reset"> + <properties> + <help>Reset a service</help> + </properties> <children> <node name="dns"> <properties> diff --git a/op-mode-definitions/generate-ssh-server-key.xml b/op-mode-definitions/generate-ssh-server-key.xml new file mode 100644 index 000000000..a6ebf1b78 --- /dev/null +++ b/op-mode-definitions/generate-ssh-server-key.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="generate"> + <properties> + <help>Generate an object</help> + </properties> + <children> + <node name="ssh-server-key"> + <properties> + <help>Regenerate the host SSH keys and restart the SSH server</help> + </properties> + <command>${vyos_op_scripts_dir}/generate_ssh_server_key.py</command> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/ipv4-route.xml b/op-mode-definitions/ipv4-route.xml new file mode 100644 index 000000000..d2846a6f2 --- /dev/null +++ b/op-mode-definitions/ipv4-route.xml @@ -0,0 +1,125 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <properties> + <help>Show system information</help> + </properties> + <children> + <node name="ip"> + <properties> + <help>Show IPv4 information</help> + </properties> + <children> + <leafNode name="groups"> + <properties> + <help>Show IP multicast group membership</help> + </properties> + <command>netstat -gn4</command> + </leafNode> + + <node name="route"> + <properties> + <help>Show IP routes</help> + </properties> + <children> + <node name="cache"> + <properties> + <help>Show kernel route cache</help> + </properties> + <command>ip -s route list cache</command> + </node> + <tagNode name="cache"> + <properties> + <help>Show kernel route cache for a given route</help> + <completionHelp> + <list><x.x.x.x> <x.x.x.x/x></list> + </completionHelp> + </properties> + <command>ip -s route list cache $5</command> + </tagNode> + <node name="forward"> + <properties> + <help>Show kernel route table</help> + </properties> + <command>ip route list</command> + </node> + <tagNode name="forward"> + <properties> + <help>Show kernel route table for a given route</help> + <completionHelp> + <list><x.x.x.x> <x.x.x.x/x></list> + </completionHelp> + </properties> + <command>ip -s route list $5</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> + + <node name="reset"> + <properties> + <help>Reset a service</help> + </properties> + <children> + <node name="ip"> + <properties> + <help>Reset Internet Protocol (IP) parameters</help> + </properties> + <children> + <node name="arp"> + <properties> + <help>Reset Address Resolution Protocol (ARP) cache</help> + </properties> + <children> + <tagNode name="address"> + <properties> + <help>Reset ARP cache for an IPv4 address</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>sudo /sbin/ip neigh flush to "$5"</command> + </tagNode> + <tagNode name="interface"> + <properties> + <help>Reset ARP cache for interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <command>sudo /sbin/ip neigh flush dev "$5"</command> + </tagNode> + </children> + </node> + + <node name="route"> + <properties> + <help>Reset IP route</help> + </properties> + <children> + <leafNode name= "cache"> + <properties> + <help>Flush the kernel route cache</help> + </properties> + <command>sudo /sbin/ip route flush cache</command> + </leafNode> + + <tagNode name="cache"> + <properties> + <help>Flush the kernel route cache for a given route</help> + <completionHelp> + <list><x.x.x.x> <x.x.x.x/x></list> + </completionHelp> + </properties> + <command>sudo /sbin/ip route flush cache "$5"</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/ipv6-route.xml b/op-mode-definitions/ipv6-route.xml new file mode 100644 index 000000000..fbf6489ba --- /dev/null +++ b/op-mode-definitions/ipv6-route.xml @@ -0,0 +1,133 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <properties> + <help>Show system information</help> + </properties> + <children> + <node name="ipv6"> + <properties> + <help>Show IPv6 routing information</help> + </properties> + <children> + <leafNode name="groups"> + <properties> + <help>Show IPv6 multicast group membership</help> + </properties> + <command>netstat -gn6</command> + </leafNode> + + <leafNode name="neighbors"> + <properties> + <help>Show IPv6 Neighbor Discovery (ND) information</help> + </properties> + <command>ip -f inet6 neigh list</command> + </leafNode> + + <node name="route"> + <properties> + <help>Show IPv6 routes</help> + </properties> + <children> + <node name="cache"> + <properties> + <help>Show kernel IPv6 route cache</help> + </properties> + <command>ip -s -f inet6 route list cache</command> + </node> + <tagNode name="cache"> + <properties> + <help>Show kernel IPv6 route cache for a given route</help> + <completionHelp> + <list><h:h:h:h:h:h:h:h> <h:h:h:h:h:h:h:h/x></list> + </completionHelp> + </properties> + <command>ip -s -f inet6 route list cache $5</command> + </tagNode> + <node name="forward"> + <properties> + <help>Show kernel IPv6 route table</help> + </properties> + <command>ip -f inet6 route list</command> + </node> + <tagNode name="forward"> + <properties> + <help>Show kernel IPv6 route table for a given route</help> + <completionHelp> + <list><h:h:h:h:h:h:h:h> <h:h:h:h:h:h:h:h/x></list> + </completionHelp> + </properties> + <command>ip -s -f inet6 route list $5</command> + </tagNode> + </children> + </node> + + </children> + </node> + </children> + </node> + + <node name="reset"> + <properties> + <help>Reset a service</help> + </properties> + <children> + <node name="ipv6"> + <properties> + <help>Reset Internet Protocol version 6 (IPv6) parameters</help> + </properties> + <children> + <node name="neighbors"> + <properties> + <help>Reset IPv6 Neighbor Discovery (ND) cache</help> + </properties> + <children> + <tagNode name="address"> + <properties> + <help>Reset ND cache for an IPv6 address</help> + <completionHelp> + <list><h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>sudo ip -f inet6 neigh flush to "$5"</command> + </tagNode> + <tagNode name="interface"> + <properties> + <help>Reset IPv6 ND cache for interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <command>sudo ip -f inet6 neigh flush dev "$5"</command> + </tagNode> + </children> + </node> + + <node name="route"> + <properties> + <help>Reset IPv6 route</help> + </properties> + <children> + <leafNode name= "cache"> + <properties> + <help>Flush the kernel IPv6 route cache</help> + </properties> + <command>sudo ip -f inet6 route flush cache</command> + </leafNode> + + <tagNode name="cache"> + <properties> + <help>Flush the kernel IPv6 route cache for a given route</help> + <completionHelp> + <list><h:h:h:h:h:h:h:h> <h:h:h:h:h:h:h:h/x></list> + </completionHelp> + </properties> + <command>sudo ip -f inet6 route flush cache "$5"</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/openvpn.xml b/op-mode-definitions/openvpn.xml index 4c958257a..368cc9115 100644 --- a/op-mode-definitions/openvpn.xml +++ b/op-mode-definitions/openvpn.xml @@ -2,25 +2,25 @@ <interfaceDefinition> <node name="generate"> <children> - <node name="openvpn"> - <properties> - <help>OpenVPN key generation tool</help> - </properties> - <children> - <tagNode name="key"> - <properties> - <help>Generate shared-secret key with specified file name</help> - <completionHelp> - <list><filename></list> - </completionHelp> - </properties> - <command> + <node name="openvpn"> + <properties> + <help>OpenVPN key generation tool</help> + </properties> + <children> + <tagNode name="key"> + <properties> + <help>Generate shared-secret key with specified file name</help> + <completionHelp> + <list><filename></list> + </completionHelp> + </properties> + <command> result=1; key_path=$4 full_path= # Prepend /config/auth if the path is not absolute - if echo $key_path | egrep -ve '^/.*' > /dev/null; then + if echo $key_path | egrep -ve '^/.*' > /dev/null; then full_path=/config/auth/$key_path else full_path=$key_path @@ -40,12 +40,15 @@ fi /usr/libexec/vyos/validators/file-exists --directory /config/auth "$full_path" </command> - </tagNode> - </children> - </node> + </tagNode> + </children> + </node> </children> </node> <node name="reset"> + <properties> + <help>Reset a service</help> + </properties> <children> <node name="openvpn"> <children> @@ -56,7 +59,7 @@ <script>sudo ${vyos_completion_dir}/list_openvpn_clients.py --all</script> </completionHelp> </properties> - <command>echo kill $4 | socat - UNIX-CONNECT:/tmp/openvpn-mgmt-intf > /dev/null</command> + <command>echo kill $4 | socat - UNIX-CONNECT:/tmp/openvpn-mgmt-intf > /dev/null</command> </tagNode> <tagNode name="interface"> <properties> @@ -107,6 +110,31 @@ </tagNode> </children> </node> + <node name="openvpn"> + <properties> + <help>Show OpenVPN information</help> + </properties> + <children> + <leafNode name="client"> + <properties> + <help>Show tunnel status for OpenVPN client interfaces</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=client</command> + </leafNode> + <leafNode name="server"> + <properties> + <help>Show tunnel status for OpenVPN server interfaces</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=server</command> + </leafNode> + <leafNode name="site-to-site"> + <properties> + <help>Show tunnel status for OpenVPN site-to-site interfaces</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=site-to-site</command> + </leafNode> + </children> + </node> </children> </node> </interfaceDefinition> diff --git a/op-mode-definitions/reset-conntrack.xml b/op-mode-definitions/reset-conntrack.xml new file mode 100644 index 000000000..827ba4af4 --- /dev/null +++ b/op-mode-definitions/reset-conntrack.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="reset"> + <properties> + <help>Reset a service</help> + </properties> + <children> + <node name="conntrack"> + <properties> + <help>Reset all currently tracked connections</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/clear_conntrack.py</command> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-disk.xml b/op-mode-definitions/show-disk.xml deleted file mode 100644 index 37da07fbe..000000000 --- a/op-mode-definitions/show-disk.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0"?> -<interfaceDefinition> - <node name="show"> - <children> - <tagNode name="disk"> - <properties> - <help>Show status of disk device</help> - <completionHelp> - <script>${vyos_completion_dir}/list_disks.sh</script> - </completionHelp> - </properties> - <children> - <leafNode name="format"> - <properties> - <help>Show disk drive formatting</help> - </properties> - <command>${vyos_op_scripts_dir}/show_disk_format.sh $3</command> - </leafNode> - </children> - </tagNode> - </children> - </node> -</interfaceDefinition> diff --git a/op-mode-definitions/show-history.xml b/op-mode-definitions/show-history.xml new file mode 100644 index 000000000..7fb286264 --- /dev/null +++ b/op-mode-definitions/show-history.xml @@ -0,0 +1,31 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="history"> + <properties> + <help>Show command history</help> + </properties> + <command>HISTTIMEFORMAT='%FT%T%z ' HISTFILE="$HOME/.bash_history" \set -o history; history</command> + <children> + <leafNode name="brief"> + <properties> + <help>Show recent command history</help> + </properties> + <command>HISTTIMEFORMAT='%FT%T%z ' HISTFILE="$HOME/.bash_history" \set -o history; history 20</command> + </leafNode> + </children> + </node> + + <tagNode name="history"> + <properties> + <help>Show last N commands in history</help> + <completionHelp> + <list><NUMBER></list> + </completionHelp> + </properties> + <command>HISTTIMEFORMAT='%FT%T%z ' HISTFILE="$HOME/.bash_history" \set -o history; history $3</command> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-host.xml b/op-mode-definitions/show-host.xml index d7f8104aa..eee1288a1 100644 --- a/op-mode-definitions/show-host.xml +++ b/op-mode-definitions/show-host.xml @@ -7,6 +7,12 @@ <help>Show host information</help> </properties> <children> + <leafNode name="date"> + <properties> + <help>Show host current date</help> + </properties> + <command>/bin/date</command> + </leafNode> <leafNode name="domain"> <properties> <help>Show domain name</help> @@ -25,6 +31,12 @@ </properties> <command>/usr/bin/host $4</command> </tagNode> + <leafNode name="os"> + <properties> + <help>Show host operating system details</help> + </properties> + <command>/bin/uname -a</command> + </leafNode> </children> </node> </children> diff --git a/op-mode-definitions/telnet.xml b/op-mode-definitions/telnet.xml new file mode 100644 index 000000000..c5bb6d283 --- /dev/null +++ b/op-mode-definitions/telnet.xml @@ -0,0 +1,30 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="telnet"> + <properties> + <help>Telnet to a node</help> + </properties> + <children> + <tagNode name="to"> + <properties> + <help>Telnet to a host</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>/usr/bin/telnet $3</command> + <children> + <tagNode name="port"> + <properties> + <help>Telnet to a host:port</help> + <completionHelp> + <list><0-65535></list> + </completionHelp> + </properties> + <command>/usr/bin/telnet $3 $5</command> + </tagNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/terminal.xml b/op-mode-definitions/terminal.xml index db74f867e..9c4e629cb 100644 --- a/op-mode-definitions/terminal.xml +++ b/op-mode-definitions/terminal.xml @@ -26,4 +26,97 @@ </node> </children> </node> + <node name="set"> + <properties> + <help>Set operational options</help> + </properties> + <children> + <tagNode name="builtin"> + <properties> + <help>Bash builtin set command</help> + <completionHelp> + <list><OPTION></list> + </completionHelp> + </properties> + <command>builtin $3</command> + </tagNode> + + <node name="console"> + <properties> + <help>Control console behaviors</help> + </properties> + <children> + <leafNode name="keymap"> + <properties> + <help>Reconfigure console keyboard layout</help> + </properties> + <command>sudo dpkg-reconfigure -f dialog keyboard-configuration && sudo systemctl restart keyboard-setup</command> + </leafNode> + </children> + </node> + + <node name="terminal"> + <properties> + <help>Control terminal behaviors</help> + </properties> + <children> + + <node name="key"> + <properties> + <help>Set key behaviors</help> + </properties> + <children> + <tagNode name="query-help"> + <properties> + <help>Enable/disable getting help using question mark (default enabled)</help> + <completionHelp> + <list>enable disable</list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/toggle_help_binding.sh $5</command> + </tagNode> + </children> + </node> + + <node name="pager"> + <properties> + <help>Set terminal pager to default (less)</help> + </properties> + <command>VYATTA_PAGER=${_vyatta_default_pager}</command> + </node> + <tagNode name="pager"> + <properties> + <help>Set terminal pager</help> + <completionHelp> + <list><PROGRAM></list> + </completionHelp> + </properties> + <command>VYATTA_PAGER=$4</command> + </tagNode> + + <tagNode name="length"> + <properties> + <help>Set terminal to given number of rows (0 disables paging)</help> + <completionHelp> + <list><NUMBER></list> + </completionHelp> + </properties> + <command>if [ "$4" -eq 0 ]; then VYATTA_PAGER=cat; else VYATTA_PAGER=${_vyatta_default_pager}; stty rows $4; fi</command> + </tagNode> + + <tagNode name="width"> + <properties> + <help>Set terminal to given number of columns</help> + <completionHelp> + <list><NUMBER></list> + </completionHelp> + </properties> + <command>stty columns $4</command> + </tagNode> + </children> + </node> + </children> + </node> + + </interfaceDefinition> diff --git a/op-mode-definitions/traceroute.xml b/op-mode-definitions/traceroute.xml new file mode 100644 index 000000000..85f6047c1 --- /dev/null +++ b/op-mode-definitions/traceroute.xml @@ -0,0 +1,53 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="traceroute"> + <properties> + <help>Track network path to node</help> + </properties> + <children> + <tagNode name=""> + <properties> + <help>Track network path to specified node</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>/usr/bin/traceroute $2</command> + </tagNode> + + <tagNode name="ipv4"> + <properties> + <help>Track network path to <hostname|IPv4 address></help> + <completionHelp> + <list><hostname> <x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/traceroute -4 $3</command> + </tagNode> + + <tagNode name="ipv6"> + <properties> + <help>Track network path to <hostname|IPv6 address></help> + <completionHelp> + <list><hostname> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>/usr/bin/traceroute -6 $3</command> + </tagNode> + </children> + </node> + + <node name="monitor"> + <children> + <tagNode name="traceroute"> + <properties> + <help>Monitor the path to a destination in realtime</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>/usr/bin/mtr $3</command> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/wireguard.xml b/op-mode-definitions/wireguard.xml index 681bb5f47..c5c4c9914 100644 --- a/op-mode-definitions/wireguard.xml +++ b/op-mode-definitions/wireguard.xml @@ -8,18 +8,24 @@ <help>wireguard key generation utility</help> </properties> <children> - <leafNode name="keypair"> + <leafNode name="default-keypair"> <properties> - <help>generate a wireguard keypair</help> + <help>generates the wireguard default-keypair</help> </properties> - <command>${vyos_op_scripts_dir}/wireguard.py --genkey</command> + <command>sudo ${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> + </leafNode> + <tagNode name="named-keypairs"> + <properties> + <help>Generates named wireguard keypairs</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/wireguard.py --genkey --location "$4"</command> + </tagNode> </children> </node> </children> @@ -31,18 +37,31 @@ <help>Show wireguard properties</help> </properties> <children> - <leafNode name="pubkey"> - <properties> - <help>show wireguard public key</help> - </properties> - <command>${vyos_op_scripts_dir}/wireguard.py --showpub</command> - </leafNode> - <leafNode name="privkey"> + <node name="keypairs"> <properties> - <help>show wireguard private key</help> + <help>Shows named wireguard keys</help> </properties> - <command>${vyos_op_scripts_dir}/wireguard.py --showpriv</command> - </leafNode> + <children> + <tagNode name="pubkey"> + <properties> + <help>Show wireguard private named key</help> + <completionHelp> + <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/wireguard.py --showpub --location "$5"</command> + </tagNode> + <tagNode name="privkey"> + <properties> + <help>Show wireguard public named key</help> + <completionHelp> + <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/wireguard.py --showpriv --location "$5"</command> + </tagNode> + </children> + </node> </children> </node> <node name="interfaces"> @@ -51,7 +70,7 @@ <properties> <help>show wireguard interface information</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py -t wireguard</script> + <script>${vyos_completion_dir}/list_interfaces.py --type wireguard</script> </completionHelp> </properties> <command>sudo wg show "$4"</command> @@ -74,12 +93,32 @@ </properties> <command>sudo wg show "$4" peers</command> </leafNode> - <!-- more commands upon request --> + <!-- more commands upon request --> </children> </tagNode> </children> </node> </children> </node> + <node name="delete"> + <children> + <node name="wireguard"> + <properties> + <help>Delete wireguard properties</help> + </properties> + <children> + <tagNode name="keypair"> + <properties> + <help>Delete a wireguard keypair</help> + <completionHelp> + <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/wireguard.py --delkdir --location "$4"</command> + </tagNode> + </children> + </node> + </children> + </node> </interfaceDefinition> diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 157011839..4bc8863bb 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -18,6 +18,7 @@ A library for retrieving value dicts from VyOS configs in a declarative fashion. """ +from vyos import ConfigError def retrieve_config(path_hash, base_path, config): """ @@ -78,3 +79,118 @@ def retrieve_config(path_hash, base_path, config): config_hash[k][node] = retrieve_config(inner_hash, path + [node], config) return config_hash + + +def list_diff(first, second): + """ + Diff two dictionaries and return only unique items + """ + second = set(second) + return [item for item in first if item not in second] + + +def get_ethertype(ethertype_val): + if ethertype_val == '0x88A8': + return '802.1ad' + elif ethertype_val == '0x8100': + return '802.1q' + else: + raise ConfigError('invalid ethertype "{}"'.format(ethertype_val)) + + +def vlan_to_dict(conf): + """ + Common used function which will extract VLAN related information from config + and represent the result as Python dictionary. + + Function call's itself recursively if a vif-s/vif-c pair is detected. + """ + vlan = { + 'id': conf.get_level().split()[-1], # get the '100' in 'interfaces bonding bond0 vif-s 100' + 'address': [], + 'address_remove': [], + 'description': '', + 'dhcp_client_id': '', + 'dhcp_hostname': '', + 'dhcpv6_prm_only': False, + 'dhcpv6_temporary': False, + 'disable': False, + 'disable_link_detect': 1, + 'mac': '', + 'mtu': 1500 + } + # retrieve configured interface addresses + if conf.exists('address'): + vlan['address'] = conf.return_values('address') + + # Determine interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed from the bond + eff_addr = conf.return_effective_values('address') + act_addr = conf.return_values('address') + vlan['address_remove'] = list_diff(eff_addr, act_addr) + + # retrieve interface description + if conf.exists('description'): + vlan['description'] = conf.return_value('description') + + # get DHCP client identifier + if conf.exists('dhcp-options client-id'): + vlan['dhcp_client_id'] = conf.return_value('dhcp-options client-id') + + # DHCP client host name (overrides the system host name) + if conf.exists('dhcp-options host-name'): + vlan['dhcp_hostname'] = conf.return_value('dhcp-options host-name') + + # DHCPv6 only acquire config parameters, no address + if conf.exists('dhcpv6-options parameters-only'): + vlan['dhcpv6_prm_only'] = conf.return_value('dhcpv6-options parameters-only') + + # DHCPv6 temporary IPv6 address + if conf.exists('dhcpv6-options temporary'): + vlan['dhcpv6_temporary'] = conf.return_value('dhcpv6-options temporary') + + # ignore link state changes + if conf.exists('disable-link-detect'): + vlan['disable_link_detect'] = 2 + + # disable bond interface + if conf.exists('disable'): + vlan['disable'] = True + + # Media Access Control (MAC) address + if conf.exists('mac'): + vlan['mac'] = conf.return_value('mac') + + # Maximum Transmission Unit (MTU) + if conf.exists('mtu'): + vlan['mtu'] = int(conf.return_value('mtu')) + + # ethertype is mandatory on vif-s nodes and only exists here! + # check if this is a vif-s node at all: + if conf.get_level().split()[-2] == 'vif-s': + vlan['vif_c'] = [] + vlan['vif_c_remove'] = [] + + # ethertype uses a default of 0x88A8 + tmp = '0x88A8' + if conf.exists('ethertype'): + tmp = conf.return_value('ethertype') + vlan['ethertype'] = get_ethertype(tmp) + + # get vif-c interfaces (currently effective) - to determine which vif-c + # interface is no longer present and needs to be removed + eff_intf = conf.list_effective_nodes('vif-c') + act_intf = conf.list_nodes('vif-c') + vlan['vif_c_remove'] = list_diff(eff_intf, act_intf) + + # check if there is a Q-in-Q vlan customer interface + # and call this function recursively + if conf.exists('vif-c'): + cfg_level = conf.get_level() + # add new key (vif-c) to dictionary + for vif in conf.list_nodes('vif-c'): + # set config level to vif interface + conf.set_level(cfg_level + ' vif-c ' + vif) + vlan['vif_c'].append(vlan_to_dict(conf)) + + return vlan diff --git a/python/vyos/configinterface.py b/python/vyos/configinterface.py deleted file mode 100644 index 0f5b0842c..000000000 --- a/python/vyos/configinterface.py +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright 2019 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 os -import vyos.validate - -def validate_mac_address(addr): - # a mac address consits out of 6 octets - octets = len(addr.split(':')) - if octets != 6: - raise ValueError('wrong number of MAC octets: {} '.format(octets)) - - # validate against the first mac address byte if it's a multicast address - if int(addr.split(':')[0]) & 1: - raise ValueError('{} is a multicast MAC address'.format(addr)) - - # overall mac address is not allowed to be 00:00:00:00:00:00 - if sum(int(i, 16) for i in addr.split(':')) == 0: - raise ValueError('00:00:00:00:00:00 is not a valid MAC address') - - # check for VRRP mac address - if addr.split(':')[0] == '0' and addr.split(':')[1] == '0' and addr.split(':')[2] == '94' and addr.split(':')[3] == '0' and addr.split(':')[4] == '1': - raise ValueError('{} is a VRRP MAC address') - - pass - -def set_mac_address(intf, addr): - """ - Configure interface mac address using iproute2 command - """ - validate_mac_address(addr) - - os.system('ip link set {} address {}'.format(intf, addr)) - pass - -def set_description(intf, desc): - """ - Sets the interface secription reported usually by SNMP - """ - with open('/sys/class/net/' + intf + '/ifalias', 'w') as f: - f.write(desc) - - pass - -def set_arp_cache_timeout(intf, tmoMS): - """ - Configure the ARP cache entry timeout in milliseconds - """ - with open('/proc/sys/net/ipv4/neigh/' + intf + '/base_reachable_time_ms', 'w') as f: - f.write(tmoMS) - - pass - -def set_multicast_querier(intf, enable): - """ - Sets whether the bridge actively runs a multicast querier or not. When a - bridge receives a 'multicast host membership' query from another network host, - that host is tracked based on the time that the query was received plus the - multicast query interval time. - - use enable=1 to enable or enable=0 to disable - """ - - if int(enable) >= 0 and int(enable) <= 1: - with open('/sys/devices/virtual/net/' + intf + '/bridge/multicast_querier', 'w') as f: - f.write(str(enable)) - else: - raise ValueError("malformed configuration string on interface {}: enable={}".format(intf, enable)) - - pass - -def set_link_detect(intf, enable): - """ - 0 - Allow packets to be received for the address on this interface - even if interface is disabled or no carrier. - - 1 - Ignore packets received if interface associated with the incoming - address is down. - - 2 - Ignore packets received if interface associated with the incoming - address is down or has no carrier. - - Kernel Source: Documentation/networking/ip-sysctl.txt - """ - - # Note can't use sysctl it is broken for vif name because of dots - # link_filter values: - # 0 - always receive - # 1 - ignore receive if admin_down - # 2 - ignore receive if admin_down or link down - - with open('/proc/sys/net/ipv4/conf/' + intf + '/link_filter', 'w') as f: - if enable == True or enable == 1: - f.write('2') - if os.path.isfile('/usr/bin/vtysh'): - os.system('/usr/bin/vtysh -c "configure terminal" -c "interface {}" -c "link-detect"'.format(intf)) - else: - f.write('1') - if os.path.isfile('/usr/bin/vtysh'): - os.system('/usr/bin/vtysh -c "configure terminal" -c "interface {}" -c "no link-detect"'.format(intf)) - - pass - -def add_interface_address(intf, addr): - """ - Configure an interface IPv4/IPv6 address - """ - if addr == "dhcp": - os.system('/opt/vyatta/sbin/vyatta-interfaces.pl --dev="{}" --dhcp=start'.format(intf)) - elif addr == "dhcpv6": - os.system('/opt/vyatta/sbin/vyatta-dhcpv6-client.pl --start -ifname "{}"'.format(intf)) - elif vyos.validate.is_ipv4(addr): - if not vyos.validate.is_intf_addr_assigned(intf, addr): - print("Assigning {} to {}".format(addr, intf)) - os.system('sudo ip -4 addr add "{}" broadcast + dev "{}"'.format(addr, intf)) - elif vyos.validate.is_ipv6(addr): - if not vyos.validate.is_intf_addr_assigned(intf, addr): - print("Assigning {} to {}".format(addr, intf)) - os.system('sudo ip -6 addr add "{}" dev "{}"'.format(addr, intf)) - else: - raise ConfigError('{} is not a valid interface address'.format(addr)) - - pass - -def remove_interface_address(intf, addr): - """ - Remove IPv4/IPv6 address from given interface - """ - - if addr == "dhcp": - os.system('/opt/vyatta/sbin/vyatta-interfaces.pl --dev="{}" --dhcp=stop'.format(intf)) - elif addr == "dhcpv6": - os.system('/opt/vyatta/sbin/vyatta-dhcpv6-client.pl --stop -ifname "{}"'.format(intf)) - elif vyos.validate.is_ipv4(addr): - os.system('ip -4 addr del "{}" dev "{}"'.format(addr, intf)) - elif vyos.validate.is_ipv6(addr): - os.system('ip -6 addr del "{}" dev "{}"'.format(addr, intf)) - else: - raise ConfigError('{} is not a valid interface address'.format(addr)) - - pass diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 8626839f2..acbdd3d5f 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -24,6 +24,7 @@ COMMENT = '/opt/vyatta/sbin/my_comment' COMMIT = '/opt/vyatta/sbin/my_commit' DISCARD = '/opt/vyatta/sbin/my_discard' SHOW_CONFIG = ['/bin/cli-shell-api', 'showConfig'] +LOAD_CONFIG = ['/bin/cli-shell-api', 'loadFile'] # Default "commit via" string APP = "vyos-http-api" @@ -155,3 +156,5 @@ class ConfigSession(object): if format == 'raw': return config_data + def load_config(self, file_path): + self.__run_command(LOAD_CONFIG + [file_path]) diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index a812b62ec..8832a5a63 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -185,6 +185,14 @@ class ConfigTree(object): return self.__to_commands(self.__config).decode() def set(self, path, value=None, replace=True): + """Set new entry in VyOS configuration. + path: configuration path e.g. 'system dns forwarding listen-address' + value: value to be added to node, e.g. '172.18.254.201' + replace: True: current occurance will be replaced + False: new value will be appended to current occurances - use + this for adding values to a multi node + """ + check_path(path) path_str = " ".join(map(str, path)).encode() diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index 3e4c02562..85d27d60d 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -29,7 +29,7 @@ cfg_vintage = 'vyatta' commit_lock = '/opt/vyatta/config/.lock' https_data = { - 'listen_address' : [ '127.0.0.1' ] + 'listen_addresses' : { '*': ['_'] } } api_data = { diff --git a/python/vyos/hostsd_client.py b/python/vyos/hostsd_client.py new file mode 100644 index 000000000..f009aba98 --- /dev/null +++ b/python/vyos/hostsd_client.py @@ -0,0 +1,69 @@ +import json + +import zmq + + +SOCKET_PATH = "ipc:///run/vyos-hostsd.sock" + + +class VyOSHostsdError(Exception): + pass + + +class Client(object): + def __init__(self): + try: + context = zmq.Context() + self.__socket = context.socket(zmq.REQ) + self.__socket.RCVTIMEO = 10000 #ms + self.__socket.setsockopt(zmq.LINGER, 0) + self.__socket.connect(SOCKET_PATH) + except zmq.error.Again: + raise VyOSHostsdError("Could not connect to vyos-hostsd") + + def _communicate(self, msg): + try: + request = json.dumps(msg).encode() + self.__socket.send(request) + + reply_msg = self.__socket.recv().decode() + reply = json.loads(reply_msg) + if 'error' in reply: + raise VyOSHostsdError(reply['error']) + else: + return reply["data"] + except zmq.error.Again: + raise VyOSHostsdError("Could not connect to vyos-hostsd") + + def set_host_name(self, host_name, domain_name, search_domains): + msg = { + 'type': 'host_name', + 'op': 'set', + 'data': { + 'host_name': host_name, + 'domain_name': domain_name, + 'search_domains': search_domains + } + } + self._communicate(msg) + + def add_hosts(self, tag, hosts): + msg = {'type': 'hosts', 'op': 'add', 'tag': tag, 'data': hosts} + self._communicate(msg) + + def delete_hosts(self, tag): + msg = {'type': 'hosts', 'op': 'delete', 'tag': tag} + self._communicate(msg) + + def add_name_servers(self, tag, servers): + msg = {'type': 'name_servers', 'op': 'add', 'tag': tag, 'data': servers} + self._communicate(msg) + + def delete_name_servers(self, tag): + msg = {'type': 'name_servers', 'op': 'delete', 'tag': tag} + self._communicate(msg) + + def get_name_servers(self, tag): + msg = {'type': 'name_servers', 'op': 'get', 'tag': tag} + return self._communicate(msg) + diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py new file mode 100644 index 000000000..62bf94d79 --- /dev/null +++ b/python/vyos/ifconfig.py @@ -0,0 +1,1449 @@ +# Copyright 2019 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 os +import re +import subprocess +import jinja2 + +from vyos.validate import * +from ipaddress import IPv4Network, IPv6Address +from netifaces import ifaddresses, AF_INET, AF_INET6 +from time import sleep + +dhcp_cfg = """ +# generated by ifconfig.py +option rfc3442-classless-static-routes code 121 = array of unsigned integer 8; +interface "{{ intf }}" { + send host-name "{{ hostname }}"; + request subnet-mask, broadcast-address, routers, domain-name-servers, rfc3442-classless-static-routes, domain-name, interface-mtu; +} +""" + +dhcpv6_cfg = """ +# generated by ifconfig.py +interface "{{ intf }}" { + request routers, domain-name-servers, domain-name; +} +""" + +dhclient_base = r'/var/lib/dhcp/dhclient_' + + +class Interface: + + def __init__(self, ifname, type=None): + """ + This is the base interface class which supports basic IP/MAC address + operations as well as DHCP(v6). Other interface which represent e.g. + and ethernet bridge are implemented as derived classes adding all + additional functionality. + + DEBUG: + This class has embedded debugging (print) which can be enabled by + creating the following file: + vyos@vyos# touch /tmp/vyos.ifconfig.debug + + Example: + >>> from vyos.ifconfig import Interface + >>> i = Interface('eth0') + """ + self._ifname = str(ifname) + self._state = 'down' + + if not os.path.exists('/sys/class/net/{}'.format(ifname)) and not type: + raise Exception('interface "{}" not found'.format(self._ifname)) + + if not os.path.exists('/sys/class/net/{}'.format(self._ifname)): + cmd = 'ip link add dev {} type {}'.format(self._ifname, type) + self._cmd(cmd) + + # per interface DHCP config files + self._dhcp_cfg_file = dhclient_base + self._ifname + '.conf' + self._dhcp_pid_file = dhclient_base + self._ifname + '.pid' + self._dhcp_lease_file = dhclient_base + self._ifname + '.leases' + + # per interface DHCPv6 config files + self._dhcpv6_cfg_file = dhclient_base + self._ifname + '.v6conf' + self._dhcpv6_pid_file = dhclient_base + self._ifname + '.v6pid' + self._dhcpv6_lease_file = dhclient_base + self._ifname + '.v6leases' + + def _debug_msg(self, msg): + if os.path.isfile('/tmp/vyos.ifconfig.debug'): + print('DEBUG/{:<6} {}'.format(self._ifname, msg)) + + def remove(self): + """ + Remove interface from operating system. Removing the interface + deconfigures all assigned IP addresses and clear possible DHCP(v6) + client processes. + + Example: + >>> from vyos.ifconfig import Interface + >>> i = Interface('eth0') + >>> i.remove() + """ + + # do we have sub interfaces (VLANs)? + # we apply a regex matching subinterfaces (indicated by a .) of a + # parent interface. 'bond0(?:\.\d+){1,2}' will match vif and vif-s/vif-c + # subinterfaces + vlan_ifs = [f for f in os.listdir(r'/sys/class/net') \ + if re.match(self._ifname + r'(?:\.\d+){1,2}', f)] + + for vlan in vlan_ifs: + Interface(vlan).remove() + + # All subinterfaces are now removed, continue on the physical interface + + # stop DHCP(v6) if running + self._del_dhcp() + self._del_dhcpv6() + + # NOTE (Improvement): + # after interface removal no other commands should be allowed + # to be called and instead should raise an Exception: + cmd = 'ip link del dev {}'.format(self._ifname) + self._cmd(cmd) + + def _cmd(self, command): + self._debug_msg("cmd '{}'".format(command)) + + process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True) + proc_stdout = process.communicate()[0].strip() + + # add exception handling code + pass + + def _read_sysfs(self, filename): + """ + Provide a single primitive w/ error checking for reading from sysfs. + """ + value = None + with open(filename, 'r') as f: + value = f.read().rstrip('\n') + + self._debug_msg("read '{}' < '{}'".format(value, filename)) + return value + + def _write_sysfs(self, filename, value): + """ + Provide a single primitive w/ error checking for writing to sysfs. + """ + self._debug_msg("write '{}' > '{}'".format(value, filename)) + with open(filename, 'w') as f: + f.write(str(value)) + + return None + + @property + def mtu(self): + """ + Get/set interface mtu in bytes. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').mtu + '1500' + """ + return self._read_sysfs('/sys/class/net/{0}/mtu' + .format(self._ifname)) + + @mtu.setter + def mtu(self, mtu): + """ + Get/set interface mtu in bytes. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').mtu = 1400 + >>> Interface('eth0').mtu + '1400' + """ + if mtu < 68 or mtu > 9000: + raise ValueError('Invalid MTU size: "{}"'.format(mru)) + + return self._write_sysfs('/sys/class/net/{0}/mtu' + .format(self._ifname), mtu) + + @property + def mac(self): + """ + Get/set interface mac address + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').mac + '00:0c:29:11:aa:cc' + """ + return self._read_sysfs('/sys/class/net/{0}/address' + .format(self._ifname)) + + @mac.setter + def mac(self, mac): + """ + Get/set interface mac address + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').mac = '00:90:43:fe:fe:1b' + >>> Interface('eth0').mac + '00:90:43:fe:fe:1b' + """ + # a mac address consits out of 6 octets + octets = len(mac.split(':')) + if octets != 6: + raise ValueError('wrong number of MAC octets: {} '.format(octets)) + + # validate against the first mac address byte if it's a multicast + # address + if int(mac.split(':')[0]) & 1: + raise ValueError('{} is a multicast MAC address'.format(mac)) + + # overall mac address is not allowed to be 00:00:00:00:00:00 + if sum(int(i, 16) for i in mac.split(':')) == 0: + raise ValueError('00:00:00:00:00:00 is not a valid MAC address') + + # check for VRRP mac address + if mac.split(':')[0] == '0' and addr.split(':')[1] == '0' and mac.split(':')[2] == '94' and mac.split(':')[3] == '0' and mac.split(':')[4] == '1': + raise ValueError('{} is a VRRP MAC address'.format(mac)) + + # Assemble command executed on system. Unfortunately there is no way + # of altering the MAC address via sysfs + cmd = 'ip link set dev {} address {}'.format(self._ifname, mac) + self._cmd(cmd) + + @property + def arp_cache_tmo(self): + """ + Get configured ARP cache timeout value from interface in seconds. + Internal Kernel representation is in milliseconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').arp_cache_tmo + '30' + """ + return (self._read_sysfs('/proc/sys/net/ipv4/neigh/{0}/base_reachable_time_ms' + .format(self._ifname)) / 1000) + + @arp_cache_tmo.setter + def arp_cache_tmo(self, tmo): + """ + Set ARP cache timeout value in seconds. Internal Kernel representation + is in milliseconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').arp_cache_tmo = '40' + """ + return self._write_sysfs('/proc/sys/net/ipv4/neigh/{0}/base_reachable_time_ms' + .format(self._ifname), (int(tmo) * 1000)) + + @property + def link_detect(self): + """ + How does the kernel act when receiving packets on 'down' interfaces + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').link_detect + '0' + """ + return self._read_sysfs('/proc/sys/net/ipv4/conf/{0}/link_filter' + .format(self._ifname)) + + @link_detect.setter + def link_detect(self, link_filter): + """ + Konfigure kernel response in packets received on interfaces that are 'down' + + 0 - Allow packets to be received for the address on this interface + even if interface is disabled or no carrier. + + 1 - Ignore packets received if interface associated with the incoming + address is down. + + 2 - Ignore packets received if interface associated with the incoming + address is down or has no carrier. + + Default value is 0. Note that some distributions enable it in startup + scripts. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').link_detect = '1' + """ + if link_filter >= 0 and link_filter <= 2: + return self._write_sysfs('/proc/sys/net/ipv4/conf/{0}/link_filter' + .format(self._ifname), link_filter) + else: + raise ValueError("Value out of range") + + @property + def ifalias(self): + """ + Get/set interface alias name + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').ifalias + '' + """ + return self._read_sysfs('/sys/class/net/{0}/ifalias' + .format(self._ifname)) + + @ifalias.setter + def ifalias(self, ifalias=None): + """ + Get/set interface alias name + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').ifalias = 'VyOS upstream interface' + >>> Interface('eth0').ifalias + 'VyOS upstream interface' + + to clear interface alias e.g. delete it use: + + >>> Interface('eth0').ifalias = '' + >>> Interface('eth0').ifalias + '' + """ + if not ifalias: + # clear interface alias + ifalias = '\0' + + self._write_sysfs('/sys/class/net/{0}/ifalias' + .format(self._ifname), ifalias) + + @property + def state(self): + """ + Enable (up) / Disable (down) an interface + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').state + 'up' + """ + return self._read_sysfs('/sys/class/net/{0}/operstate' + .format(self._ifname)) + + @state.setter + def state(self, state): + """ + Enable (up) / Disable (down) an interface + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').state = 'down' + >>> Interface('eth0').state + 'down' + """ + if state not in ['up', 'down']: + raise ValueError('state must be "up" or "down"') + + self._state = state + + # Assemble command executed on system. Unfortunately there is no way + # to up/down an interface via sysfs + cmd = 'ip link set dev {} {}'.format(self._ifname, state) + self._cmd(cmd) + + @property + def proxy_arp(self): + """ + Get current proxy ARP configuration from sysfs. Default: 0 + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').proxy_arp + '0' + """ + return self._read_sysfs('/proc/sys/net/ipv4/conf/{}/proxy_arp' + .format(self._ifname)) + + @proxy_arp.setter + def proxy_arp(self, enable): + """ + Set per interface proxy ARP configuration + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').proxy_arp = 1 + >>> Interface('eth0').proxy_arp + '1' + """ + if int(enable) >= 0 and int(enable) <= 1: + return self._write_sysfs('/proc/sys/net/ipv4/conf/{}/proxy_arp' + .format(self._ifname), enable) + else: + raise ValueError("Value out of range") + + @property + def proxy_arp_pvlan(self): + """ + Private VLAN proxy arp. + Basically allow proxy arp replies back to the same interface + (from which the ARP request/solicitation was received). + + This is done to support (ethernet) switch features, like RFC + 3069, where the individual ports are NOT allowed to + communicate with each other, but they are allowed to talk to + the upstream router. As described in RFC 3069, it is possible + to allow these hosts to communicate through the upstream + router by proxy_arp'ing. Don't need to be used together with + proxy_arp. + + This technology is known by different names: + In RFC 3069 it is called VLAN Aggregation. + Cisco and Allied Telesyn call it Private VLAN. + Hewlett-Packard call it Source-Port filtering or port-isolation. + Ericsson call it MAC-Forced Forwarding (RFC Draft). + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').proxy_arp_pvlan + '0' + """ + return self._read_sysfs('/proc/sys/net/ipv4/conf/{}/proxy_arp_pvlan' + .format(self._ifname)) + + @proxy_arp_pvlan.setter + def proxy_arp_pvlan(self, enable): + """ + Private VLAN proxy arp. + Basically allow proxy arp replies back to the same interface + (from which the ARP request/solicitation was received). + + This is done to support (ethernet) switch features, like RFC + 3069, where the individual ports are NOT allowed to + communicate with each other, but they are allowed to talk to + the upstream router. As described in RFC 3069, it is possible + to allow these hosts to communicate through the upstream + router by proxy_arp'ing. Don't need to be used together with + proxy_arp. + + This technology is known by different names: + In RFC 3069 it is called VLAN Aggregation. + Cisco and Allied Telesyn call it Private VLAN. + Hewlett-Packard call it Source-Port filtering or port-isolation. + Ericsson call it MAC-Forced Forwarding (RFC Draft). + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').proxy_arp_pvlan = 1 + >>> Interface('eth0').proxy_arp_pvlan + '1' + """ + if int(enable) >= 0 and int(enable) <= 1: + return self._write_sysfs('/proc/sys/net/ipv4/conf/{}/proxy_arp_pvlan' + .format(self._ifname), enable) + else: + raise ValueError("Value out of range") + + def get_addr(self): + """ + Retrieve assigned IPv4 and IPv6 addresses from given interface. + This is done using the netifaces and ipaddress python modules. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_addrs() + ['172.16.33.30/24', 'fe80::20c:29ff:fe11:a174/64'] + """ + + ipv4 = [] + ipv6 = [] + + if AF_INET in ifaddresses(self._ifname).keys(): + for v4_addr in ifaddresses(self._ifname)[AF_INET]: + # we need to manually assemble a list of IPv4 address/prefix + prefix = '/' + \ + str(IPv4Network('0.0.0.0/' + v4_addr['netmask']).prefixlen) + ipv4.append(v4_addr['addr'] + prefix) + + if AF_INET6 in ifaddresses(self._ifname).keys(): + for v6_addr in ifaddresses(self._ifname)[AF_INET6]: + # Note that currently expanded netmasks are not supported. That means + # 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not. + # see https://docs.python.org/3/library/ipaddress.html + bits = bin( + int(v6_addr['netmask'].replace(':', ''), 16)).count('1') + prefix = '/' + str(bits) + + # we alsoneed to remove the interface suffix on link local + # addresses + v6_addr['addr'] = v6_addr['addr'].split('%')[0] + ipv6.append(v6_addr['addr'] + prefix) + + return ipv4 + ipv6 + + def add_addr(self, addr): + """ + Add IP(v6) address to interface. Address is only added if it is not + already assigned to that interface. + + addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! + IPv4: add IPv4 address to interface + IPv6: add IPv6 address to interface + dhcp: start dhclient (IPv4) on interface + dhcpv6: start dhclient (IPv6) on interface + + Example: + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.add_addr('192.0.2.1/24') + >>> j.add_addr('2001:db8::ffff/64') + >>> j.get_addr() + ['192.0.2.1/24', '2001:db8::ffff/64'] + """ + if addr == 'dhcp': + self._set_dhcp() + elif addr == 'dhcpv6': + self._set_dhcpv6() + else: + if not is_intf_addr_assigned(self._ifname, addr): + cmd = 'ip addr add "{}" dev "{}"'.format(addr, self._ifname) + self._cmd(cmd) + + def del_addr(self, addr): + """ + Delete IP(v6) address to interface. Address is only added if it is + assigned to that interface. + + addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! + IPv4: delete IPv4 address from interface + IPv6: delete IPv6 address from interface + dhcp: stop dhclient (IPv4) on interface + dhcpv6: stop dhclient (IPv6) on interface + + Example: + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.add_addr('2001:db8::ffff/64') + >>> j.add_addr('192.0.2.1/24') + >>> j.get_addr() + ['192.0.2.1/24', '2001:db8::ffff/64'] + >>> j.del_addr('192.0.2.1/24') + >>> j.get_addr() + ['2001:db8::ffff/64'] + """ + if addr == 'dhcp': + self._del_dhcp() + elif addr == 'dhcpv6': + self._del_dhcpv6() + else: + if is_intf_addr_assigned(self._ifname, addr): + cmd = 'ip addr del "{}" dev "{}"'.format(addr, self._ifname) + self._cmd(cmd) + + # replace dhcpv4/v6 with systemd.networkd? + def _set_dhcp(self): + """ + Configure interface as DHCP client. The dhclient binary is automatically + started in background! + + Example: + + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.set_dhcp() + """ + dhcp = { + 'hostname': 'vyos', + 'intf': self._ifname + } + + # read configured system hostname. + # maybe change to vyos hostd client ??? + with open('/etc/hostname', 'r') as f: + dhcp['hostname'] = f.read().rstrip('\n') + + # render DHCP configuration + tmpl = jinja2.Template(dhcp_cfg) + dhcp_text = tmpl.render(dhcp) + with open(self._dhcp_cfg_file, 'w') as f: + f.write(dhcp_text) + + if self._state == 'up': + cmd = 'start-stop-daemon --start --quiet --pidfile ' + \ + self._dhcp_pid_file + cmd += ' --exec /sbin/dhclient --' + # now pass arguments to dhclient binary + cmd += ' -4 -nw -cf {} -pf {} -lf {} {}'.format( + self._dhcp_cfg_file, self._dhcp_pid_file, self._dhcp_lease_file, self._ifname) + self._cmd(cmd) + + + def _del_dhcp(self): + """ + De-configure interface as DHCP clinet. All auto generated files like + pid, config and lease will be removed. + + Example: + + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.del_dhcp() + """ + pid = 0 + if os.path.isfile(self._dhcp_pid_file): + with open(self._dhcp_pid_file, 'r') as f: + pid = int(f.read()) + else: + self._debug_msg('No DHCP client PID found') + return None + + # stop dhclient + cmd = 'start-stop-daemon --stop --quiet --pidfile {}'.format( + self._dhcp_pid_file) + self._cmd(cmd) + + # cleanup old config file + if os.path.isfile(self._dhcp_cfg_file): + os.remove(self._dhcp_cfg_file) + + # cleanup old pid file + if os.path.isfile(self._dhcp_pid_file): + os.remove(self._dhcp_pid_file) + + # cleanup old lease file + if os.path.isfile(self._dhcp_lease_file): + os.remove(self._dhcp_lease_file) + + + def _set_dhcpv6(self): + """ + Configure interface as DHCPv6 client. The dhclient binary is automatically + started in background! + + Example: + + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.set_dhcpv6() + """ + dhcpv6 = { + 'intf': self._ifname + } + + # render DHCP configuration + tmpl = jinja2.Template(dhcpv6_cfg) + dhcpv6_text = tmpl.render(dhcpv6) + with open(self._dhcpv6_cfg_file, 'w') as f: + f.write(dhcpv6_text) + + if self._state == 'up': + # https://bugs.launchpad.net/ubuntu/+source/ifupdown/+bug/1447715 + # + # wee need to wait for IPv6 DAD to finish once and interface is added + # this suxx :-( + sleep(5) + + # no longer accept router announcements on this interface + cmd = 'sysctl -q -w net.ipv6.conf.{}.accept_ra=0'.format(self._ifname) + self._cmd(cmd) + + # assemble command-line to start DHCPv6 client (dhclient) + cmd = 'start-stop-daemon --start --quiet --pidfile ' + \ + self._dhcpv6_pid_file + cmd += ' --exec /sbin/dhclient --' + # now pass arguments to dhclient binary + cmd += ' -6 -nw -cf {} -pf {} -lf {} {}'.format( + self._dhcpv6_cfg_file, self._dhcpv6_pid_file, self._dhcpv6_lease_file, self._ifname) + self._cmd(cmd) + + + def _del_dhcpv6(self): + """ + De-configure interface as DHCPv6 clinet. All auto generated files like + pid, config and lease will be removed. + + Example: + + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.del_dhcpv6() + """ + pid = 0 + if os.path.isfile(self._dhcpv6_pid_file): + with open(self._dhcpv6_pid_file, 'r') as f: + pid = int(f.read()) + else: + self._debug_msg('No DHCPv6 client PID found') + return None + + # stop dhclient + cmd = 'start-stop-daemon --stop --quiet --pidfile {}'.format( + self._dhcpv6_pid_file) + self._cmd(cmd) + + # accept router announcements on this interface + cmd = 'sysctl -q -w net.ipv6.conf.{}.accept_ra=1'.format(self._ifname) + self._cmd(cmd) + + # cleanup old config file + if os.path.isfile(self._dhcpv6_cfg_file): + os.remove(self._dhcpv6_cfg_file) + + # cleanup old pid file + if os.path.isfile(self._dhcpv6_pid_file): + os.remove(self._dhcpv6_pid_file) + + # cleanup old lease file + if os.path.isfile(self._dhcpv6_lease_file): + os.remove(self._dhcpv6_lease_file) + + +class LoopbackIf(Interface): + + """ + The loopback device is a special, virtual network interface that your router + uses to communicate with itself. + """ + + def __init__(self, ifname): + super().__init__(ifname, type='loopback') + + +class DummyIf(Interface): + + """ + A dummy interface is entirely virtual like, for example, the loopback + interface. The purpose of a dummy interface is to provide a device to route + packets through without actually transmitting them. + """ + + def __init__(self, ifname): + super().__init__(ifname, type='dummy') + + +class BridgeIf(Interface): + + """ + A bridge is a way to connect two Ethernet segments together in a protocol + independent way. Packets are forwarded based on Ethernet address, rather + than IP address (like a router). Since forwarding is done at Layer 2, all + protocols can go transparently through a bridge. + + The Linux bridge code implements a subset of the ANSI/IEEE 802.1d standard. + """ + + def __init__(self, ifname): + super().__init__(ifname, type='bridge') + + @property + def ageing_time(self): + """ + Return configured bridge interface MAC address aging time in seconds. + Internal kernel representation is in centiseconds, thus its converted + in the end. Kernel default is 300 seconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').aging_time + '300' + """ + return (self._read_sysfs('/sys/class/net/{0}/bridge/ageing_time' + .format(self._ifname)) / 100) + + @ageing_time.setter + def ageing_time(self, time): + """ + Set bridge interface MAC address aging time in seconds. Internal kernel + representation is in centiseconds. Kernel default is 300 seconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').ageing_time = 2 + """ + time = int(time) * 100 + return self._write_sysfs('/sys/class/net/{0}/bridge/ageing_time' + .format(self._ifname), time) + + @property + def forward_delay(self): + """ + Get bridge forwarding delay in seconds. Internal Kernel representation + is in centiseconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').ageing_time + '3' + """ + return (self._read_sysfs('/sys/class/net/{0}/bridge/forward_delay' + .format(self._ifname)) / 100) + + @forward_delay.setter + def forward_delay(self, time): + """ + Set bridge forwarding delay in seconds. Internal Kernel representation + is in centiseconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').forward_delay = 15 + """ + return self._write_sysfs('/sys/class/net/{0}/bridge/forward_delay' + .format(self._ifname), (int(time) * 100)) + + @property + def hello_time(self): + """ + Get bridge hello time in seconds. Internal Kernel representation + is in centiseconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').hello_time + '2' + """ + return (self._read_sysfs('/sys/class/net/{0}/bridge/hello_time' + .format(self._ifname)) / 100) + + @hello_time.setter + def hello_time(self, time): + """ + Set bridge hello time in seconds. Internal Kernel representation + is in centiseconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').hello_time = 2 + """ + return self._write_sysfs('/sys/class/net/{0}/bridge/hello_time' + .format(self._ifname), (int(time) * 100)) + + @property + def max_age(self): + """ + Get bridge max max message age in seconds. Internal Kernel representation + is in centiseconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').max_age + '20' + """ + + return (self._read_sysfs('/sys/class/net/{0}/bridge/max_age' + .format(self._ifname)) / 100) + + @max_age.setter + def max_age(self, time): + """ + Set bridge max message age in seconds. Internal Kernel representation + is in centiseconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').max_age = 30 + """ + return self._write_sysfs('/sys/class/net/{0}/bridge/max_age' + .format(self._ifname), (int(time) * 100)) + + @property + def priority(self): + """ + Get bridge max aging time in seconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').priority + '32768' + """ + return self._read_sysfs('/sys/class/net/{0}/bridge/priority' + .format(self._ifname)) + + @priority.setter + def priority(self, priority): + """ + Set bridge max aging time in seconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').priority = 8192 + """ + return self._write_sysfs('/sys/class/net/{0}/bridge/priority' + .format(self._ifname), priority) + + @property + def stp_state(self): + """ + Get current bridge STP (Spanning Tree) state. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').stp_state + '0' + """ + + state = 0 + with open('/sys/class/net/{0}/bridge/stp_state'.format(self._ifname), 'r') as f: + state = int(f.read().rstrip('\n')) + + return state + + @stp_state.setter + def stp_state(self, state): + """ + Set bridge STP (Spannign Tree) state. 0 -> STP disabled, 1 -> STP enabled + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').stp_state = 1 + """ + + if int(state) >= 0 and int(state) <= 1: + return self._write_sysfs('/sys/class/net/{0}/bridge/stp_state' + .format(self._ifname), state) + else: + raise ValueError("Value out of range") + + @property + def multicast_querier(self): + """ + Get bridge multicast querier membership state. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').multicast_querier + '0' + """ + return self._read_sysfs('/sys/class/net/{0}/bridge/multicast_querier' + .format(self._ifname)) + + @multicast_querier.setter + def multicast_querier(self, enable): + """ + Sets whether the bridge actively runs a multicast querier or not. When a + bridge receives a 'multicast host membership' query from another network + host, that host is tracked based on the time that the query was received + plus the multicast query interval time. + + Use enable=1 to enable or enable=0 to disable + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').multicast_querier = 1 + """ + if int(enable) >= 0 and int(enable) <= 1: + return self._write_sysfs('/sys/class/net/{0}/bridge/multicast_querier' + .format(self._ifname), enable) + else: + raise ValueError("Value out of range") + + def add_port(self, interface): + """ + Add physical interface to bridge (member port) + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').add_port('eth0') + >>> BridgeIf('br0').add_port('eth1') + """ + cmd = 'ip link set dev {} master {}'.format(interface, self._ifname) + self._cmd(cmd) + + def del_port(self, interface): + """ + Remove member port from bridge instance. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').del_port('eth1') + """ + cmd = 'ip link set dev {} nomaster'.format(interface) + self._cmd(cmd) + + def set_cost(self, interface, cost): + """ + Set interface path cost, only relevant for STP enabled interfaces + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').path_cost(4) + """ + return self._write_sysfs('/sys/class/net/{}/brif/{}/path_cost' + .format(self._ifname, interface), cost) + + def set_priority(self, interface, priority): + """ + Set interface path priority, only relevant for STP enabled interfaces + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').priority(4) + """ + return self._write_sysfs('/sys/class/net/{}/brif/{}/priority' + .format(self._ifname, interface), priority) + + +class EthernetIf(Interface): + + def __init__(self, ifname, type=None): + super().__init__(ifname, type) + + def add_vlan(self, vlan_id, ethertype=''): + """ + A virtual LAN (VLAN) is any broadcast domain that is partitioned and + isolated in a computer network at the data link layer (OSI layer 2). + Use this function to create a new VLAN interface on a given physical + interface. + + This function creates both 802.1q and 802.1ad (Q-in-Q) interfaces. Proto + parameter is used to indicate VLAN type. + + A new object of type EthernetIf is returned once the interface has been + created. + """ + vlan_ifname = self._ifname + '.' + str(vlan_id) + if not os.path.exists('/sys/class/net/{}'.format(vlan_ifname)): + self._vlan_id = int(vlan_id) + + if ethertype: + self._ethertype = ethertype + ethertype = 'proto {}'.format(ethertype) + + # create interface in the system + cmd = 'ip link add link {intf} name {intf}.{vlan} type vlan {proto} id {vlan}'.format( + intf=self._ifname, vlan=self._vlan_id, proto=ethertype) + self._cmd(cmd) + + # return new object mapping to the newly created interface + # we can now work on this object for e.g. IP address setting + # or interface description and so on + return EthernetIf(vlan_ifname) + + def del_vlan(self, vlan_id): + """ + Remove VLAN interface from operating system. Removing the interface + deconfigures all assigned IP addresses and clear possible DHCP(v6) + client processes. + """ + vlan_ifname = self._ifname + '.' + str(vlan_id) + tmp = EthernetIf(vlan_ifname) + tmp.remove() + + +class BondIf(EthernetIf): + + """ + The Linux bonding driver provides a method for aggregating multiple network + interfaces into a single logical "bonded" interface. The behavior of the + bonded interfaces depends upon the mode; generally speaking, modes provide + either hot standby or load balancing services. Additionally, link integrity + monitoring may be performed. + """ + + def __init__(self, ifname): + super().__init__(ifname, type='bond') + + @property + def xmit_hash_policy(self): + """ + Selects the transmit hash policy to use for slave selection in + balance-xor, 802.3ad, and tlb modes. Possible values are: layer2, + layer2+3, layer3+4, encap2+3, encap3+4. + + The default value is layer2 + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').xmit_hash_policy + 'layer3+4' + """ + # Linux Kernel appends has policy value to string, e.g. 'layer3+4 1', + # so remove the later part and only return the mode as string. + return self._read_sysfs('/sys/class/net/{}/bonding/xmit_hash_policy' + .format(self._ifname)).split()[0] + + @xmit_hash_policy.setter + def xmit_hash_policy(self, mode): + """ + Selects the transmit hash policy to use for slave selection in + balance-xor, 802.3ad, and tlb modes. Possible values are: layer2, + layer2+3, layer3+4, encap2+3, encap3+4. + + The default value is layer2 + + Example: + >>> from vyos.ifconfig import Interface + >>> BondIf('bond0').xmit_hash_policy = 'layer2+3' + >>> BondIf('bond0').proxy_arp + '1' + """ + if not mode in ['layer2', 'layer2+3', 'layer3+4', 'encap2+3', 'encap3+4']: + raise ValueError("Value out of range") + return self._write_sysfs('/sys/class/net/{}/bonding/xmit_hash_policy' + .format(self._ifname), mode) + + @property + def arp_interval(self): + """ + Specifies the ARP link monitoring frequency in milliseconds. + + The ARP monitor works by periodically checking the slave devices to + determine whether they have sent or received traffic recently (the + precise criteria depends upon the bonding mode, and the state of the + slave). Regular traffic is generated via ARP probes issued for the + addresses specified by the arp_ip_target option. + + The default value is 0. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').arp_interval + '0' + """ + return self._read_sysfs('/sys/class/net/{}/bonding/arp_interval' + .format(self._ifname)) + + @arp_interval.setter + def arp_interval(self, time): + """ + Specifies the IP addresses to use as ARP monitoring peers when + arp_interval is > 0. These are the targets of the ARP request sent to + determine the health of the link to the targets. Specify these values + in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by + a comma. At least one IP address must be given for ARP monitoring to + function. The maximum number of targets that can be specified is 16. + + The default value is no IP addresses. + + Example: + >>> from vyos.ifconfig import Interface + >>> BondIf('bond0').arp_interval = '100' + >>> BondIf('bond0').arp_interval + '100' + """ + return self._write_sysfs('/sys/class/net/{}/bonding/arp_interval' + .format(self._ifname), time) + + @property + def arp_ip_target(self): + """ + Specifies the IP addresses to use as ARP monitoring peers when + arp_interval is > 0. These are the targets of the ARP request sent to + determine the health of the link to the targets. Specify these values + in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by + a comma. At least one IP address must be given for ARP monitoring to + function. The maximum number of targets that can be specified is 16. + + The default value is no IP addresses. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').arp_ip_target + '192.0.2.1' + """ + return self._read_sysfs('/sys/class/net/{}/bonding/arp_ip_target' + .format(self._ifname)) + + @arp_ip_target.setter + def arp_ip_target(self, target): + """ + Specifies the IP addresses to use as ARP monitoring peers when + arp_interval is > 0. These are the targets of the ARP request sent to + determine the health of the link to the targets. Specify these values + in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by + a comma. At least one IP address must be given for ARP monitoring to + function. The maximum number of targets that can be specified is 16. + + The default value is no IP addresses. + + Example: + >>> from vyos.ifconfig import Interface + >>> BondIf('bond0').arp_ip_target = '192.0.2.1' + >>> BondIf('bond0').arp_ip_target + '192.0.2.1' + """ + return self._write_sysfs('/sys/class/net/{}/bonding/arp_ip_target' + .format(self._ifname), target) + + def add_port(self, interface): + """ + Enslave physical interface to bond. + + Example: + >>> from vyos.ifconfig import Interface + >>> BondIf('bond0').add_port('eth0') + >>> BondIf('bond0').add_port('eth1') + """ + # An interface can only be added to a bond if it is in 'down' state. If + # interface is in 'up' state, the following Kernel error will be thrown: + # bond0: eth1 is up - this may be due to an out of date ifenslave. + Interface(interface).state = 'down' + + return self._write_sysfs('/sys/class/net/{}/bonding/slaves' + .format(self._ifname), '+' + interface) + + def del_port(self, interface): + """ + Remove physical port from bond + + Example: + >>> from vyos.ifconfig import Interface + >>> BondIf('bond0').del_port('eth1') + """ + return self._write_sysfs('/sys/class/net/{}/bonding/slaves' + .format(self._ifname), '-' + interface) + + def get_slaves(self): + """ + Return a list with all configured slave interfaces on this bond. + + Example: + >>> from vyos.ifconfig import Interface + >>> BondIf('bond0').get_slaves() + ['eth1', 'eth2'] + """ + slaves = self._read_sysfs('/sys/class/net/{}/bonding/slaves' + .format(self._ifname)) + return list(map(str, slaves.split())) + + @property + def primary(self): + """ + A string (eth0, eth2, etc) specifying which slave is the primary + device. The specified device will always be the active slave while it + is available. Only when the primary is off-line will alternate devices + be used. This is useful when one slave is preferred over another, e.g., + when one slave has higher throughput than another. + + The primary option is only valid for active-backup, balance-tlb and + balance-alb mode. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').primary + 'eth1' + """ + return self._read_sysfs('/sys/class/net/{}/bonding/primary' + .format(self._ifname)) + + @primary.setter + def primary(self, interface): + """ + A string (eth0, eth2, etc) specifying which slave is the primary + device. The specified device will always be the active slave while it + is available. Only when the primary is off-line will alternate devices + be used. This is useful when one slave is preferred over another, e.g., + when one slave has higher throughput than another. + + The primary option is only valid for active-backup, balance-tlb and + balance-alb mode. + + Example: + >>> from vyos.ifconfig import Interface + >>> BondIf('bond0').primary = 'eth2' + >>> BondIf('bond0').primary + 'eth2' + """ + if not interface: + # reset primary interface + interface = '\0' + + return self._write_sysfs('/sys/class/net/{}/bonding/primary' + .format(self._ifname), interface) + + @property + def mode(self): + """ + Specifies one of the bonding policies. The default is balance-rr + (round robin). + + Possible values are: balance-rr (0), active-backup (1), balance-xor (2), + broadcast (3), 802.3ad (4), balance-tlb (5), balance-alb (6) + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').mode + 'balance-rr' + """ + return self._read_sysfs('/sys/class/net/{}/bonding/mode' + .format(self._ifname)).split()[0] + + @mode.setter + def mode(self, mode): + """ + Specifies one of the bonding policies. The default is balance-rr + (round robin). + + Possible values are: balance-rr, active-backup, balance-xor, + broadcast, 802.3ad, balance-tlb, balance-alb + + NOTE: the bonding mode can not be changed when the bond itself has + slaves + + Example: + >>> from vyos.ifconfig import Interface + >>> BondIf('bond0').mode = '802.3ad' + >>> BondIf('bond0').mode + '802.3ad' + """ + if not mode in [ + 'balance-rr', 'active-backup', 'balance-xor', 'broadcast', + '802.3ad', 'balance-tlb', 'balance-alb']: + raise ValueError("Value out of range") + + return self._write_sysfs('/sys/class/net/{}/bonding/mode' + .format(self._ifname), mode) + + +class WireGuardIf(Interface): + """ + Wireguard interface class, contains a comnfig dictionary since + wireguard VPN is being comnfigured via the wg command rather than + writing the config into a file. Otherwise if a pre-shared key is used + (symetric enryption key), it would we exposed within multiple files. + Currently it's only within the config.boot if the config was saved. + + Example: + >>> from vyos.ifconfig import WireGuardIf as wg_if + >>> wg_intfc = wg_if("wg01") + >>> print (wg_intfc.wg_config) + {'private-key': None, 'keepalive': 0, 'endpoint': None, 'port': 0, + 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} + >>> wg_intfc.wg_config['keepalive'] = 100 + >>> print (wg_intfc.wg_config) + {'private-key': None, 'keepalive': 100, 'endpoint': None, 'port': 0, + 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} + """ + + def __init__(self, ifname): + super().__init__(ifname, type='wireguard') + self.config = { + 'port': 0, + 'private-key': None, + 'pubkey': None, + 'psk': '/dev/null', + 'allowed-ips': [], + 'fwmark': 0x00, + 'endpoint': None, + 'keepalive': 0 + } + + def update(self): + if not self.config['private-key']: + raise ValueError("private key required") + else: + # fmask permission check? + pass + + cmd = "wg set {} ".format(self._ifname) + cmd += "listen-port {} ".format(self.config['port']) + cmd += "fwmark {} ".format(str(self.config['fwmark'])) + cmd += "private-key {} ".format(self.config['private-key']) + cmd += "peer {} ".format(self.config['pubkey']) + cmd += " preshared-key {} ".format(self.config['psk']) + cmd += " allowed-ips " + for aip in self.config['allowed-ips']: + if aip != self.config['allowed-ips'][-1]: + cmd += aip + "," + else: + cmd += aip + if self.config['endpoint']: + cmd += " endpoint {}".format(self.config['endpoint']) + cmd += " persistent-keepalive {}".format(self.config['keepalive']) + + self._cmd(cmd) + + # remove psk since it isn't required anymore and is saved in the cli + # config only !! + if self.config['psk'] != '/dev/null': + if os.path.exists(self.config['psk']): + os.remove(self.config['psk']) + + + def remove_peer(self, peerkey): + """ + Remove a peer of an interface, peers are identified by their public key. + Giving it a readable name is a vyos feature, to remove a peer the pubkey + and the interface is needed, to remove the entry. + """ + cmd = "wg set {0} peer {1} remove".format( + self._ifname, str(peerkey)) + self._cmd(cmd) + + +class VXLANIf(Interface, ): + """ + The VXLAN protocol is a tunnelling protocol designed to solve the + problem of limited VLAN IDs (4096) in IEEE 802.1q. With VXLAN the + size of the identifier is expanded to 24 bits (16777216). + + VXLAN is described by IETF RFC 7348, and has been implemented by a + number of vendors. The protocol runs over UDP using a single + destination port. This document describes the Linux kernel tunnel + device, there is also a separate implementation of VXLAN for + Openvswitch. + + Unlike most tunnels, a VXLAN is a 1 to N network, not just point to + point. A VXLAN device can learn the IP address of the other endpoint + either dynamically in a manner similar to a learning bridge, or make + use of statically-configured forwarding entries. + + For more information please refer to: + https://www.kernel.org/doc/Documentation/networking/vxlan.txt + """ + def __init__(self, ifname, config=''): + if config: + self._ifname = ifname + + if not os.path.exists('/sys/class/net/{}'.format(self._ifname)): + # we assume that by default a multicast interface is created + group = 'group {}'.format(config['group']) + + # if remote host is specified we ignore the multicast address + if config['remote']: + group = 'remote {}'.format(config['remote']) + + # an underlay device is not always specified + dev = '' + if config['dev']: + dev = 'dev {}'.format(config['dev']) + + cmd = 'ip link add {intf} type vxlan id {vni} {grp_rem} {dev} dstport {port}' \ + .format(intf=self._ifname, vni=config['vni'], grp_rem=group, dev=dev, port=config['port']) + self._cmd(cmd) + + super().__init__(ifname, type='vxlan') + + @staticmethod + def get_config(): + """ + VXLAN interfaces require a configuration when they are added using + iproute2. This static method will provide the configuration dictionary + used by this class. + + Example: + >> dict = VXLANIf().get_config() + """ + config = { + 'vni': 0, + 'dev': '', + 'group': '', + 'port': 8472, # The Linux implementation of VXLAN pre-dates + # the IANA's selection of a standard destination port + 'remote': '' + } + return config diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py deleted file mode 100644 index b8bfb707e..000000000 --- a/python/vyos/interfaceconfig.py +++ /dev/null @@ -1,376 +0,0 @@ -#!/usr/bin/python3 - -# Copyright 2019 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 sys -import os -import re -import json -import socket -import subprocess - -dhclient_conf_dir = r'/var/lib/dhcp/dhclient_' - -class Interface: - def __init__(self, ifname=None, type=None): - if not ifname: - raise Exception("interface name required") - if not os.path.exists('/sys/class/net/{0}'.format(ifname)) and not type: - raise Exception("interface {0} not found".format(str(ifname))) - else: - if not os.path.exists('/sys/class/net/{0}'.format(ifname)): - try: - ret = subprocess.check_output(['ip link add dev ' + str(ifname) + ' type ' + type], stderr=subprocess.STDOUT, shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - if "Operation not supported" in str(e.output.decode()): - print(str(e.output.decode())) - sys.exit(0) - - self._ifname = str(ifname) - - - @property - def mtu(self): - return self._mtu - - @mtu.setter - def mtu(self, mtu=None): - if mtu < 68 or mtu > 9000: - raise ValueError("mtu size invalid value") - self._mtu = mtu - try: - ret = subprocess.check_output(['ip link set mtu ' + str(mtu) + ' dev ' + self._ifname], shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - - - @property - def macaddr(self): - return self._macaddr - - @macaddr.setter - def macaddr(self, mac=None): - if not re.search('^[a-f0-9:]{17}$', str(mac)): - raise ValueError("mac address invalid") - self._macaddr = str(mac) - try: - ret = subprocess.check_output(['ip link set address ' + mac + ' ' + self._ifname], shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - - @property - def ifalias(self): - return self._ifalias - - @ifalias.setter - def ifalias(self, ifalias=None): - if not ifalias: - self._ifalias = self._ifname - else: - self._ifalias = str(ifalias) - open('/sys/class/net/{0}/ifalias'.format(self._ifname),'w').write(self._ifalias) - - @property - def linkstate(self): - return self._linkstate - - @linkstate.setter - def linkstate(self, state='up'): - if str(state).lower() == 'up' or str(state).lower() == 'down': - self._linkstate = str(state).lower() - else: - self._linkstate = 'up' - try: - ret = subprocess.check_output(['ip link set dev ' + self._ifname + ' ' + state], shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - - - - def _debug(self, e=None): - """ - export DEBUG=1 to see debug messages - """ - if os.getenv('DEBUG') == '1': - if e: - print ("Exception raised:\ncommand: {0}\nerror code: {1}\nsubprocess output: {2}".format(e.cmd, e.returncode, e.output.decode()) ) - return True - return False - - def get_mtu(self): - try: - ret = subprocess.check_output(['ip -j link list dev ' + self._ifname], shell=True).decode() - a = json.loads(ret)[0] - return a['mtu'] - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def get_macaddr(self): - try: - ret = subprocess.check_output(['ip -j -4 link show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - j = json.loads(ret) - return j[0]['address'] - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def get_alias(self): - return open('/sys/class/net/{0}/ifalias'.format(self._ifname),'r').read() - - def del_alias(self): - open('/sys/class/net/{0}/ifalias'.format(self._ifname),'w').write() - - def get_link_state(self): - """ - returns either up/down or None if it can't find the state - """ - try: - ret = subprocess.check_output(['ip -j link show ' + self._ifname], shell=True).decode() - s = json.loads(ret) - return s[0]['operstate'].lower() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def remove_interface(self): - try: - ret = subprocess.check_output(['ip link del dev ' + self._ifname], shell=True).decode() - return 0 - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def get_ipv4_addr(self): - """ - reads all IPs assigned to an interface and returns it in a list, - or None if no IP address is assigned to the interface - """ - ips = [] - try: - ret = subprocess.check_output(['ip -j -4 addr show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - j = json.loads(ret) - for i in j: - if len(i) != 0: - for addr in i['addr_info']: - ips.append(addr['local']) - return ips - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - - def get_ipv6_addr(self): - """ - reads all IPs assigned to an interface and returns it in a list, - or None if no IP address is assigned to the interface - """ - ips = [] - try: - ret = subprocess.check_output(['ip -j -6 addr show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - j = json.loads(ret) - for i in j: - if len(i) != 0: - for addr in i['addr_info']: - ips.append(addr['local']) - return ips - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - - def add_ipv4_addr(self, ipaddr=[]): - """ - add addresses on the interface - """ - for ip in ipaddr: - try: - ret = subprocess.check_output(['ip -4 address add ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - return True - - - def del_ipv4_addr(self, ipaddr=[]): - """ - delete addresses on the interface - """ - for ip in ipaddr: - try: - ret = subprocess.check_output(['ip -4 address del ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - return True - - - def add_ipv6_addr(self, ipaddr=[]): - """ - add addresses on the interface - """ - for ip in ipaddr: - try: - ret = subprocess.check_output(['ip -6 address add ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - return True - - - def del_ipv6_addr(self, ipaddr=[]): - """ - delete addresses on the interface - """ - for ip in ipaddr: - try: - ret = subprocess.check_output(['ip -6 address del ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - return True - - - #### replace dhcpv4/v6 with systemd.networkd? - def set_dhcpv4(self): - conf_file = dhclient_conf_dir + self._ifname + '.conf' - pidfile = dhclient_conf_dir + self._ifname + '.pid' - leasefile = dhclient_conf_dir + self._ifname + '.leases' - - a = [ - '# generated by interface_config.py', - 'option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;', - 'interface \"' + self._ifname + '\" {', - '\tsend host-name \"' + socket.gethostname() +'\";', - '\trequest subnet-mask, broadcast-address, routers, domain-name-servers, rfc3442-classless-static-routes, domain-name, interface-mtu;', - '}' - ] - - cnf = "" - for ln in a: - cnf +=str(ln + "\n") - open(conf_file, 'w').write(cnf) - if os.path.exists(dhclient_conf_dir + self._ifname + '.pid'): - try: - ret = subprocess.check_output(['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - try: - ret = subprocess.check_output(['/sbin/dhclient -4 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self._ifname], shell=True).decode() - return True - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def del_dhcpv4(self): - conf_file = dhclient_conf_dir + self._ifname + '.conf' - pidfile = dhclient_conf_dir + self._ifname + '.pid' - leasefile = dhclient_conf_dir + self._ifname + '.leases' - if not os.path.exists(pidfile): - return 1 - try: - ret = subprocess.check_output(['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode() - return True - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def get_dhcpv4(self): - pidfile = dhclient_conf_dir + self._ifname + '.pid' - if not os.path.exists(pidfile): - print ("no dhcp client running on interface {0}".format(self._ifname)) - return False - else: - pid = open(pidfile, 'r').read() - print("dhclient running on {0} with pid {1}".format(self._ifname, pid)) - return True - - - def set_dhcpv6(self): - conf_file = dhclient_conf_dir + self._ifname + '.v6conf' - pidfile = dhclient_conf_dir + self._ifname + '.v6pid' - leasefile = dhclient_conf_dir + self._ifname + '.v6leases' - a = [ - '# generated by interface_config.py', - 'interface \"' + self._ifname + '\" {', - '\trequest routers, domain-name-servers, domain-name;', - '}' - ] - cnf = "" - for ln in a: - cnf +=str(ln + "\n") - open(conf_file, 'w').write(cnf) - subprocess.call(['sysctl', '-q', '-w', 'net.ipv6.conf.' + self._ifname + '.accept_ra=0']) - if os.path.exists(pidfile): - try: - ret = subprocess.check_output(['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - try: - ret = subprocess.check_output(['/sbin/dhclient -6 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self._ifname], shell=True).decode() - return True - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def del_dhcpv6(self): - conf_file = dhclient_conf_dir + self._ifname + '.v6conf' - pidfile = dhclient_conf_dir + self._ifname + '.v6pid' - leasefile = dhclient_conf_dir + self._ifname + '.v6leases' - if not os.path.exists(pidfile): - return 1 - try: - ret = subprocess.check_output(['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode() - subprocess.call(['sysctl', '-q', '-w', 'net.ipv6.conf.' + self._ifname + '.accept_ra=1']) - return True - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def get_dhcpv6(self): - pidfile = dhclient_conf_dir + self._ifname + '.v6pid' - if not os.path.exists(pidfile): - print ("no dhcpv6 client running on interface {0}".format(self._ifname)) - return False - else: - pid = open(pidfile, 'r').read() - print("dhclientv6 running on {0} with pid {1}".format(self._ifname, pid)) - return True - - -#### TODO: dhcpv6-pd via dhclient - diff --git a/python/vyos/interfaces.py b/python/vyos/interfaces.py index 2e8ee4feb..d69ce9d04 100644 --- a/python/vyos/interfaces.py +++ b/python/vyos/interfaces.py @@ -43,3 +43,14 @@ def list_interfaces_of_type(typ): else: r = re.compile('^{0}\d+'.format(types_data[typ])) return list(filter(lambda i: re.match(r, i), all_intfs)) + +def get_type_of_interface(intf): + with open(intf_type_data_file, 'r') as f: + types_data = json.load(f) + + for key,val in types_data.items(): + r = re.compile('^{0}\d+'.format(val)) + if re.match(r, intf): + return key + + raise ValueError("No type found for interface name: {0}".format(intf)) diff --git a/python/vyos/util.py b/python/vyos/util.py index 6ab606983..67a602f7a 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -15,9 +15,11 @@ import os import re +import getpass import grp import time import subprocess +import sys import psutil @@ -176,3 +178,24 @@ def wait_for_commit_lock(): while commit_in_progress(): time.sleep(1) +def ask_yes_no(question, default=False) -> bool: + """Ask a yes/no question via input() and return their answer.""" + default_msg = "[Y/n]" if default else "[y/N]" + while True: + sys.stdout.write("%s %s " % (question, default_msg)) + c = input().lower() + if c == '': + return default + elif c in ("y", "ye", "yes"): + return True + elif c in ("n", "no"): + return False + else: + sys.stdout.write("Please respond with yes/y or no/n\n") + + +def is_admin() -> bool: + """Look if current user is in sudo group""" + current_user = getpass.getuser() + (_, _, _, admin_group_members) = grp.getgrnam('sudo') + return current_user in admin_group_members diff --git a/python/vyos/validate.py b/python/vyos/validate.py index 97a401423..258f7f76a 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -16,6 +16,12 @@ import netifaces import ipaddress +def is_ip(addr): + """ + Check addr if it is an IPv4 or IPv6 address + """ + return is_ipv4(addr) or is_ipv6(addr) + def is_ipv4(addr): """ Check addr if it is an IPv4 address/network. Returns True/False diff --git a/src/completion/list_disks.py b/src/completion/list_disks.py new file mode 100755 index 000000000..ff1135e23 --- /dev/null +++ b/src/completion/list_disks.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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/>. + +# Completion script used by show disks to collect physical disk + +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument("-e", "--exclude", type=str, help="Exclude specified device from the result list") +args = parser.parse_args() + +disks = set() +with open('/proc/partitions') as partitions_file: + for line in partitions_file: + fields = line.strip().split() + if len(fields) == 4 and fields[3].isalpha() and fields[3] != 'name': + disks.add(fields[3]) + +if args.exclude: + disks.remove(args.exclude) + +for disk in disks: + print(disk) diff --git a/src/completion/list_disks.sh b/src/completion/list_disks.sh deleted file mode 100755 index f32e558fd..000000000 --- a/src/completion/list_disks.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -# Completion script used by show disks to collect physical disk - -awk 'NR > 2 && $4 !~ /[0-9]$/ { print $4 }' </proc/partitions diff --git a/src/completion/list_interfaces.py b/src/completion/list_interfaces.py index 66432af19..5e444ef78 100755 --- a/src/completion/list_interfaces.py +++ b/src/completion/list_interfaces.py @@ -2,15 +2,14 @@ import sys import argparse - import vyos.interfaces - parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group() group.add_argument("-t", "--type", type=str, help="List interfaces of specific type") group.add_argument("-b", "--broadcast", action="store_true", help="List all broadcast interfaces") group.add_argument("-br", "--bridgeable", action="store_true", help="List all bridgeable interfaces") +group.add_argument("-bo", "--bondable", action="store_true", help="List all bondable interfaces") args = parser.parse_args() @@ -21,11 +20,13 @@ if args.type: except ValueError as e: print(e, file=sys.stderr) print("") + elif args.broadcast: eth = vyos.interfaces.list_interfaces_of_type("ethernet") bridge = vyos.interfaces.list_interfaces_of_type("bridge") bond = vyos.interfaces.list_interfaces_of_type("bonding") interfaces = eth + bridge + bond + elif args.bridgeable: eth = vyos.interfaces.list_interfaces_of_type("ethernet") bond = vyos.interfaces.list_interfaces_of_type("bonding") @@ -34,6 +35,15 @@ elif args.bridgeable: vxlan = vyos.interfaces.list_interfaces_of_type("vxlan") wireless = vyos.interfaces.list_interfaces_of_type("wireless") interfaces = eth + bond + l2tpv3 + openvpn + vxlan + wireless + +elif args.bondable: + eth = vyos.interfaces.list_interfaces_of_type("ethernet") + # we need to filter out VLAN interfaces identified by a dot (.) in their name + for intf in eth: + if '.' in intf: + eth.remove(intf) + interfaces = eth + else: interfaces = vyos.interfaces.list_interfaces() diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 3ca77adee..38f3cb4de 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -24,6 +24,7 @@ import jinja2 import netifaces import vyos.util +import vyos.hostsd_client from vyos.config import Config from vyos import ConfigError @@ -44,7 +45,7 @@ config_tmpl = """ # Non-configurable defaults daemon=yes threads=1 -allow-from=0.0.0.0/0, ::/0 +allow-from={{ allow_from | join(',') }} log-common-errors=yes non-local-bind=yes query-local-address=0.0.0.0 @@ -83,10 +84,10 @@ dnssec={{ dnssec }} """ default_config_data = { + 'allow_from': [], 'cache_size': 10000, 'export_hosts_file': 'yes', 'listen_on': [], - 'interfaces': [], 'name_servers': [], 'negative_ttl': 3600, 'domains': [], @@ -94,19 +95,6 @@ default_config_data = { } -# borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX! -def get_resolvers(file): - try: - with open(file, 'r') as resolvconf: - lines = [line.split('#', 1)[0].rstrip() - for line in resolvconf.readlines()] - resolvers = [line.split()[1] - for line in lines if 'nameserver' in line] - return resolvers - except IOError: - return [] - - def get_config(arguments): dns = default_config_data conf = Config() @@ -121,6 +109,9 @@ def get_config(arguments): conf.set_level('service dns forwarding') + if conf.exists('allow-from'): + dns['allow_from'] = conf.return_values('allow-from') + if conf.exists('cache-size'): cache_size = conf.return_value('cache-size') dns['cache_size'] = cache_size @@ -164,64 +155,27 @@ def get_config(arguments): if conf.exists('dnssec'): dns['dnssec'] = conf.return_value('dnssec') - ## Hacks and tricks - - # The old VyOS syntax that comes from dnsmasq was "listen-on $interface". - # pdns wants addresses instead, so we emulate it by looking up all addresses - # of a given interface and writing them to the config - if conf.exists('listen-on'): - print("WARNING: since VyOS 1.2.0, \"service dns forwarding listen-on\" is a limited compatibility option.") - print("It will only make DNS forwarder listen on addresses assigned to the interface at the time of commit") - print("which means it will NOT work properly with VRRP/clustering or addresses received from DHCP.") - print("Please reconfigure your system with \"service dns forwarding listen-address\" instead.") - - interfaces = conf.return_values('listen-on') - - listen4 = [] - listen6 = [] - for interface in interfaces: - try: - addrs = netifaces.ifaddresses(interface) - except ValueError: - print( - "WARNING: interface {0} does not exist".format(interface)) - continue - - if netifaces.AF_INET in addrs.keys(): - for ip4 in addrs[netifaces.AF_INET]: - listen4.append(ip4['addr']) - - if netifaces.AF_INET6 in addrs.keys(): - for ip6 in addrs[netifaces.AF_INET6]: - listen6.append(ip6['addr']) - - if (not listen4) and (not (listen6)): - print( - "WARNING: interface {0} has no configured addresses".format(interface)) - - dns['listen_on'] = dns['listen_on'] + listen4 + listen6 - - # Save interfaces in the dict for the reference - dns['interfaces'] = interfaces - # Add name servers received from DHCP if conf.exists('dhcp'): interfaces = [] interfaces = conf.return_values('dhcp') + hc = vyos.hostsd_client.Client() + for interface in interfaces: - dhcp_resolvers = get_resolvers( - "/etc/resolv.conf.dhclient-new-{0}".format(interface)) + dhcp_resolvers = hc.get_name_servers("dhcp-{0}".format(interface)) + dhcpv6_resolvers = hc.get_name_servers("dhcpv6-{0}".format(interface)) + if dhcp_resolvers: dns['name_servers'] = dns['name_servers'] + dhcp_resolvers + if dhcpv6_resolvers: + dns['name_servers'] = dns['name_servers'] + dhcpv6_resolvers return dns - def bracketize_ipv6_addrs(addrs): """Wraps each IPv6 addr in addrs in [], leaving IPv4 addrs untouched.""" return ['[{0}]'.format(a) if a.count(':') > 1 else a for a in addrs] - def verify(dns): # bail out early - looks like removal from running config if dns is None: @@ -231,6 +185,10 @@ def verify(dns): raise ConfigError( "Error: DNS forwarding requires either a listen-address (preferred) or a listen-on option") + if not dns['allow_from']: + raise ConfigError( + "Error: DNS forwarding requires an allow-from network") + if dns['domains']: for domain in dns['domains']: if not domain['servers']: @@ -239,7 +197,6 @@ def verify(dns): return None - def generate(dns): # bail out early - looks like removal from running config if dns is None: @@ -251,16 +208,14 @@ def generate(dns): f.write(config_text) return None - def apply(dns): - if dns is not None: - os.system("systemctl restart pdns-recursor") - else: + if dns is None: # DNS forwarding is removed in the commit os.system("systemctl stop pdns-recursor") if os.path.isfile(config_file): os.unlink(config_file) - + else: + os.system("systemctl restart pdns-recursor") if __name__ == '__main__': args = parser.parse_args() diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index 2fad57db6..bb1ec9597 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -30,57 +30,12 @@ import argparse import jinja2 import vyos.util +import vyos.hostsd_client from vyos.config import Config from vyos import ConfigError -parser = argparse.ArgumentParser() -parser.add_argument("--dhclient", action="store_true", - help="Started from dhclient-script") - -config_file_hosts = '/etc/hosts' -config_file_resolv = '/etc/resolv.conf' - -config_tmpl_hosts = """ -### Autogenerated by host_name.py ### -127.0.0.1 localhost -127.0.1.1 {{ hostname }}{% if domain_name %}.{{ domain_name }} {{ hostname }}{% endif %} - -# The following lines are desirable for IPv6 capable hosts -::1 localhost ip6-localhost ip6-loopback -fe00::0 ip6-localnet -ff00::0 ip6-mcastprefix -ff02::1 ip6-allnodes -ff02::2 ip6-allrouters - -# static hostname mappings -{%- if static_host_mapping['hostnames'] %} -{% for hn in static_host_mapping['hostnames'] -%} -{{static_host_mapping['hostnames'][hn]['ipaddr']}}\t{{static_host_mapping['hostnames'][hn]['alias']}}\t{{hn}} -{% endfor -%} -{%- endif %} - -### modifications from other scripts should be added below - -""" - -config_tmpl_resolv = """ -### Autogenerated by host_name.py ### -{% for ns in nameserver -%} -nameserver {{ ns }} -{% endfor -%} - -{%- if domain_name %} -domain {{ domain_name }} -{%- endif %} - -{%- if domain_search %} -search {{ domain_search | join(" ") }} -{%- endif %} - -""" - default_config_data = { 'hostname': 'vyos', 'domain_name': '', @@ -89,32 +44,10 @@ default_config_data = { 'no_dhcp_ns': False } -# borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX! -def get_resolvers(file): - resolv = {} - try: - with open(file, 'r') as resolvconf: - lines = [line.split('#', 1)[0].rstrip() - for line in resolvconf.readlines()] - resolvers = [line.split()[1] - for line in lines if 'nameserver' in line] - domains = [line.split()[1] for line in lines if 'search' in line] - resolv['resolvers'] = resolvers - resolv['domains'] = domains - return resolv - except IOError: - return [] - - -def get_config(arguments): +def get_config(): conf = Config() hosts = copy.deepcopy(default_config_data) - if arguments.dhclient: - conf.exists = conf.exists_effective - conf.return_value = conf.return_effective_value - conf.return_values = conf.return_effective_values - if conf.exists("system host-name"): hosts['hostname'] = conf.return_value("system host-name") # This may happen if the config is not loaded yet, @@ -136,19 +69,15 @@ def get_config(arguments): hosts['no_dhcp_ns'] = conf.exists('system disable-dhcp-nameservers') # system static-host-mapping - hosts['static_host_mapping'] = {'hostnames': {}} + hosts['static_host_mapping'] = [] if conf.exists('system static-host-mapping host-name'): for hn in conf.list_nodes('system static-host-mapping host-name'): - hosts['static_host_mapping']['hostnames'][hn] = { - 'ipaddr': conf.return_value('system static-host-mapping host-name ' + hn + ' inet'), - 'alias': '' - } - - if conf.exists('system static-host-mapping host-name ' + hn + ' alias'): - a = conf.return_values( - 'system static-host-mapping host-name ' + hn + ' alias') - hosts['static_host_mapping']['hostnames'][hn]['alias'] = " ".join(a) + mapping = {} + mapping['host'] = hn + mapping['address'] = conf.return_value('system static-host-mapping host-name {0} inet'.format(hn)) + mapping['aliases'] = conf.return_values('system static-host-mapping host-name {0} alias'.format(hn)) + hosts['static_host_mapping'].append(mapping) return hosts @@ -180,83 +109,43 @@ def verify(config): 'The search list is currently limited to 256 characters') # static mappings alias hostname - if config['static_host_mapping']['hostnames']: - for hn in config['static_host_mapping']['hostnames']: - if not config['static_host_mapping']['hostnames'][hn]['ipaddr']: - raise ConfigError('IP address required for ' + hn) - for hn_alias in config['static_host_mapping']['hostnames'][hn]['alias'].split(' '): - if not hostname_regex.match(hn_alias) and len(hn_alias) != 0: - raise ConfigError('Invalid hostname alias ' + hn_alias) + if config['static_host_mapping']: + for m in config['static_host_mapping']: + if not m['address']: + raise ConfigError('IP address required for ' + m['host']) + for a in m['aliases']: + if not hostname_regex.match(a) and len(a) != 0: + raise ConfigError('Invalid alias \'{0}\' in mapping {1}'.format(a, m['host'])) return None def generate(config): + pass + +def apply(config): if config is None: return None - # If "system disable-dhcp-nameservers" is __configured__ all DNS resolvers - # received via dhclient should not be added into the final 'resolv.conf'. - # - # We iterate over every resolver file and retrieve the received nameservers - # for later adjustment of the system nameservers - dhcp_ns = [] - dhcp_sd = [] - for file in glob.glob('/etc/resolv.conf.dhclient-new*'): - for key, value in get_resolvers(file).items(): - ns = [r for r in value if key == 'resolvers'] - dhcp_ns.extend(ns) - sd = [d for d in value if key == 'domains'] - dhcp_sd.extend(sd) - - if not config['no_dhcp_ns']: - config['nameserver'] += dhcp_ns - config['domain_search'] += dhcp_sd - - # Prune duplicate values - # Not order preserving, but then when multiple DHCP clients are used, - # there can't be guarantees about the order anyway - dhcp_ns = list(set(dhcp_ns)) - dhcp_sd = list(set(dhcp_sd)) - - # We have third party scripts altering /etc/hosts, too. - # One example are the DHCP hostname update scripts thus we need to cache in - # every modification first - so changing domain-name, domain-search or hostname - # during runtime works - old_hosts = "" - with open(config_file_hosts, 'r') as f: - # Skips text before the beginning of our marker. - # NOTE: Marker __MUST__ match the one specified in config_tmpl_hosts - for line in f: - if line.strip() == '### modifications from other scripts should be added below': - break - - for line in f: - # This additional line.strip() filters empty lines - if line.strip(): - old_hosts += line - - # Add an additional newline - old_hosts += '\n' - - tmpl = jinja2.Template(config_tmpl_hosts) - config_text = tmpl.render(config) - - with open(config_file_hosts, 'w') as f: - f.write(config_text) - f.write(old_hosts) - - tmpl = jinja2.Template(config_tmpl_resolv) - config_text = tmpl.render(config) - with open(config_file_resolv, 'w') as f: - f.write(config_text) + ## Send the updated data to vyos-hostsd - return None + # vyos-hostsd uses "tags" to identify data sources + tag = "static" + try: + client = vyos.hostsd_client.Client() -def apply(config): - if config is None: - return None + client.set_host_name(config['hostname'], config['domain_name'], config['domain_search']) + + client.delete_name_servers(tag) + client.add_name_servers(tag, config['nameserver']) + + client.delete_hosts(tag) + client.add_hosts(tag, config['static_host_mapping']) + except vyos.hostsd_client.VyOSHostsdError as e: + raise ConfigError(str(e)) + + ## Actually update the hostname -- vyos-hostsd doesn't do that # No domain name -- the Debian way. hostname_new = config['hostname'] @@ -283,22 +172,9 @@ def apply(config): if __name__ == '__main__': - args = parser.parse_args() - - if args.dhclient: - # There's a big chance it was triggered by a commit still in progress - # so we need to wait until the new values are in the running config - vyos.util.wait_for_commit_lock() - - try: - c = get_config(args) - # If it's called from dhclient, then either: - # a) verification was already done at commit time - # b) it's run on an unconfigured system, e.g. by cloud-init - # Therefore, verification is either redundant or useless - if not args.dhclient: - verify(c) + c = get_config() + verify(c) generate(c) apply(c) except ConfigError as e: diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py index 1f91ac582..9c062f0aa 100755 --- a/src/conf_mode/http-api.py +++ b/src/conf_mode/http-api.py @@ -69,6 +69,9 @@ def generate(http_api): if http_api is None: return None + if not os.path.exists('/etc/vyos'): + os.mkdir('/etc/vyos') + with open(config_file, 'w') as f: json.dump(http_api, f, indent=2) diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index 289eacf69..f948063e9 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -40,12 +40,21 @@ server { return 302 https://$server_name$request_uri; } +{% for addr, names in listen_addresses.items() %} server { # SSL configuration # +{% if addr == '*' %} listen 443 ssl default_server; listen [::]:443 ssl default_server; +{% else %} + listen {{ addr }}:443 ssl; +{% endif %} + +{% for name in names %} + server_name {{ name }}; +{% endfor %} {% if vyos_cert %} include {{ vyos_cert.conf }}; @@ -57,9 +66,42 @@ server { include snippets/snakeoil.conf; {% endif %} -{% for l_addr in listen_address %} - server_name {{ l_addr }}; -{% endfor %} + # proxy settings for HTTP API, if enabled; 503, if not + location ~ /(retrieve|configure) { +{% if api %} + proxy_pass http://localhost:{{ api.port }}; + proxy_buffering off; +{% else %} + return 503; +{% endif %} + } + + error_page 501 502 503 =200 @50*_json; + + location @50*_json { + default_type application/json; + return 200 '{"error": "Start service in configuration mode: set service https api"}'; + } + +} +{% else %} +server { + # SSL configuration + # + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + server_name _; + +{% if vyos_cert %} + include {{ vyos_cert.conf }}; +{% else %} + # + # Self signed certs generated by the ssl-cert package + # Don't use them in a production server! + # + include snippets/snakeoil.conf; +{% endif %} # proxy settings for HTTP API, if enabled; 503, if not location ~ /(retrieve|configure) { @@ -79,6 +121,8 @@ server { } } + +{% endfor %} """ def get_config(): @@ -90,8 +134,13 @@ def get_config(): conf.set_level('service https') if conf.exists('listen-address'): - addrs = conf.return_values('listen-address') - https['listen_address'] = addrs[:] + addrs = {} + for addr in conf.list_nodes('listen-address'): + addrs[addr] = ['_'] + if conf.exists('listen-address {0} server-name'.format(addr)): + names = conf.return_values('listen-address {0} server-name'.format(addr)) + addrs[addr] = names[:] + https['listen_addresses'] = addrs if conf.exists('certificates'): if conf.exists('certificates system-generated-certificate'): diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py new file mode 100755 index 000000000..dc0363fb7 --- /dev/null +++ b/src/conf_mode/interface-bonding.py @@ -0,0 +1,469 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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 os + +from copy import deepcopy +from sys import exit +from netifaces import interfaces + +from vyos.ifconfig import BondIf, EthernetIf +from vyos.configdict import list_diff, vlan_to_dict +from vyos.config import Config +from vyos import ConfigError + +default_config_data = { + 'address': [], + 'address_remove': [], + 'arp_mon_intvl': 0, + 'arp_mon_tgt': [], + 'description': '', + 'deleted': False, + 'dhcp_client_id': '', + 'dhcp_hostname': '', + 'dhcpv6_prm_only': False, + 'dhcpv6_temporary': False, + 'disable': False, + 'disable_link_detect': 1, + 'hash_policy': 'layer2', + 'ip_arp_cache_tmo': 30, + 'ip_proxy_arp': 0, + 'ip_proxy_arp_pvlan': 0, + 'intf': '', + 'mac': '', + 'mode': '802.3ad', + 'member': [], + 'mtu': 1500, + 'primary': '', + 'vif_s': [], + 'vif_s_remove': [], + 'vif': [], + 'vif_remove': [] +} + + +def get_bond_mode(mode): + if mode == 'round-robin': + return 'balance-rr' + elif mode == 'active-backup': + return 'active-backup' + elif mode == 'xor-hash': + return 'balance-xor' + elif mode == 'broadcast': + return 'broadcast' + elif mode == '802.3ad': + return '802.3ad' + elif mode == 'transmit-load-balance': + return 'balance-tlb' + elif mode == 'adaptive-load-balance': + return 'balance-alb' + else: + raise ConfigError('invalid bond mode "{}"'.format(mode)) + + +def apply_vlan_config(vlan, config): + """ + Generic function to apply a VLAN configuration from a dictionary + to a VLAN interface + """ + + if type(vlan) != type(EthernetIf("lo")): + raise TypeError() + + # update interface description used e.g. within SNMP + vlan.ifalias = config['description'] + # ignore link state changes + vlan.link_detect = config['disable_link_detect'] + # Maximum Transmission Unit (MTU) + vlan.mtu = config['mtu'] + # Change VLAN interface MAC address + if config['mac']: + vlan.mac = config['mac'] + + # enable/disable VLAN interface + if config['disable']: + vlan.state = 'down' + else: + vlan.state = 'up' + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in config['address_remove']: + vlan.del_addr(addr) + for addr in config['address']: + vlan.add_addr(addr) + + +def get_config(): + # initialize kernel module if not loaded + if not os.path.isfile('/sys/class/net/bonding_masters'): + import syslog + syslog.syslog(syslog.LOG_NOTICE, "loading bonding kernel module") + if os.system('modprobe bonding max_bonds=0 miimon=250') != 0: + syslog.syslog(syslog.LOG_NOTICE, "failed loading bonding kernel module") + raise ConfigError("failed loading bonding kernel module") + + bond = deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + bond['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + # check if bond has been removed + cfg_base = 'interfaces bonding ' + bond['intf'] + if not conf.exists(cfg_base): + bond['deleted'] = True + return bond + + # set new configuration level + conf.set_level(cfg_base) + + # retrieve configured interface addresses + if conf.exists('address'): + bond['address'] = conf.return_values('address') + + # get interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed + eff_addr = conf.return_effective_values('address') + bond['address_remove'] = list_diff(eff_addr, bond['address']) + + # ARP link monitoring frequency in milliseconds + if conf.exists('arp-monitor interval'): + bond['arp_mon_intvl'] = int(conf.return_value('arp-monitor interval')) + + # IP address to use for ARP monitoring + if conf.exists('arp-monitor target'): + bond['arp_mon_tgt'] = conf.return_values('arp-monitor target') + + # retrieve interface description + if conf.exists('description'): + bond['description'] = conf.return_value('description') + else: + bond['description'] = bond['intf'] + + # get DHCP client identifier + if conf.exists('dhcp-options client-id'): + bond['dhcp_client_id'] = conf.return_value('dhcp-options client-id') + + # DHCP client host name (overrides the system host name) + if conf.exists('dhcp-options host-name'): + bond['dhcp_hostname'] = conf.return_value('dhcp-options host-name') + + # DHCPv6 only acquire config parameters, no address + if conf.exists('dhcpv6-options parameters-only'): + bond['dhcpv6_prm_only'] = conf.return_value('dhcpv6-options parameters-only') + + # DHCPv6 temporary IPv6 address + if conf.exists('dhcpv6-options temporary'): + bond['dhcpv6_temporary'] = conf.return_value('dhcpv6-options temporary') + + # ignore link state changes + if conf.exists('disable-link-detect'): + bond['disable_link_detect'] = 2 + + # disable bond interface + if conf.exists('disable'): + bond['disable'] = True + + # Bonding transmit hash policy + if conf.exists('hash-policy'): + bond['hash_policy'] = conf.return_value('hash-policy') + + # ARP cache entry timeout in seconds + if conf.exists('ip arp-cache-timeout'): + bond['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) + + # Enable proxy-arp on this interface + if conf.exists('ip enable-proxy-arp'): + bond['ip_proxy_arp'] = 1 + + # Enable private VLAN proxy ARP on this interface + if conf.exists('ip proxy-arp-pvlan'): + bond['ip_proxy_arp_pvlan'] = 1 + + # Media Access Control (MAC) address + if conf.exists('mac'): + bond['mac'] = conf.return_value('mac') + + # Bonding mode + if conf.exists('mode'): + bond['mode'] = get_bond_mode(conf.return_value('mode')) + + # Maximum Transmission Unit (MTU) + if conf.exists('mtu'): + bond['mtu'] = int(conf.return_value('mtu')) + + # determine bond member interfaces (currently configured) + if conf.exists('member interface'): + bond['member'] = conf.return_values('member interface') + + # Primary device interface + if conf.exists('primary'): + bond['primary'] = conf.return_value('primary') + + # re-set configuration level and retrieve vif-s interfaces + conf.set_level(cfg_base) + # get vif-s interfaces (currently effective) - to determine which vif-s + # interface is no longer present and needs to be removed + eff_intf = conf.list_effective_nodes('vif-s') + act_intf = conf.list_nodes('vif-s') + bond['vif_s_remove'] = list_diff(eff_intf, act_intf) + + if conf.exists('vif-s'): + for vif_s in conf.list_nodes('vif-s'): + # set config level to vif-s interface + conf.set_level(cfg_base + ' vif-s ' + vif_s) + bond['vif_s'].append(vlan_to_dict(conf)) + + # re-set configuration level and retrieve vif-s interfaces + conf.set_level(cfg_base) + # Determine vif interfaces (currently effective) - to determine which + # vif interface is no longer present and needs to be removed + eff_intf = conf.list_effective_nodes('vif') + act_intf = conf.list_nodes('vif') + bond['vif_remove'] = list_diff(eff_intf, act_intf) + + if conf.exists('vif'): + for vif in conf.list_nodes('vif'): + # set config level to vif interface + conf.set_level(cfg_base + ' vif ' + vif) + bond['vif'].append(vlan_to_dict(conf)) + + return bond + + +def verify(bond): + if len (bond['arp_mon_tgt']) > 16: + raise ConfigError('The maximum number of targets that can be specified is 16') + + if bond['primary']: + if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: + raise ConfigError('Mode dependency failed, primary not supported ' \ + 'in this mode.'.format()) + + if bond['primary'] not in bond['member']: + raise ConfigError('Interface "{}" is not part of the bond' \ + .format(bond['primary'])) + + for vif_s in bond['vif_s']: + for vif in bond['vif']: + if vif['id'] == vif_s['id']: + raise ConfigError('Can not use identical ID on vif and vif-s interface') + + + conf = Config() + for intf in bond['member']: + # a bonding member interface is only allowed to be assigned to one bond! + all_bonds = conf.list_nodes('interfaces bonding') + # We do not need to check our own bond + all_bonds.remove(bond['intf']) + for tmp in all_bonds: + if conf.exists('interfaces bonding ' + tmp + ' member interface ' + intf): + raise ConfigError('can not enslave interface {} which already ' \ + 'belongs to {}'.format(intf, tmp)) + + # we can not add disabled slave interfaces to our bond + if conf.exists('interfaces ethernet ' + intf + ' disable'): + raise ConfigError('can not enslave disabled interface {}' \ + .format(intf)) + + # can not add interfaces with an assigned address to a bond + if conf.exists('interfaces ethernet ' + intf + ' address'): + raise ConfigError('can not enslave interface {} which has an address ' \ + 'assigned'.format(intf)) + + # bond members are not allowed to be bridge members, too + for tmp in conf.list_nodes('interfaces bridge'): + if conf.exists('interfaces bridge ' + tmp + ' member interface ' + intf): + raise ConfigError('can not enslave interface {} which belongs to ' \ + 'bridge {}'.format(intf, tmp)) + + # bond members are not allowed to be vrrp members, too + for tmp in conf.list_nodes('high-availability vrrp group'): + if conf.exists('high-availability vrrp group ' + tmp + ' interface ' + intf): + raise ConfigError('can not enslave interface {} which belongs to ' \ + 'VRRP group {}'.format(intf, tmp)) + + # bond members are not allowed to be underlaying psuedo-ethernet devices + for tmp in conf.list_nodes('interfaces pseudo-ethernet'): + if conf.exists('interfaces pseudo-ethernet ' + tmp + ' link ' + intf): + raise ConfigError('can not enslave interface {} which belongs to ' \ + 'pseudo-ethernet {}'.format(intf, tmp)) + + # bond members are not allowed to be underlaying vxlan devices + for tmp in conf.list_nodes('interfaces vxlan'): + if conf.exists('interfaces vxlan ' + tmp + ' link ' + intf): + raise ConfigError('can not enslave interface {} which belongs to ' \ + 'vxlan {}'.format(intf, tmp)) + + + if bond['primary']: + if bond['primary'] not in bond['member']: + raise ConfigError('primary interface must be a member interface of {}' \ + .format(bond['intf'])) + + if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: + raise ConfigError('primary interface only works for mode active-backup, ' \ + 'transmit-load-balance or adaptive-load-balance') + + if bond['arp_mon_intvl'] > 0: + if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']: + raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \ + 'transmit-load-balance or adaptive-load-balance') + + return None + + +def generate(bond): + return None + + +def apply(bond): + b = BondIf(bond['intf']) + + if bond['deleted']: + # + # delete bonding interface + b.remove() + else: + # Some parameters can not be changed when the bond is up. + # Always disable the bond prior changing anything + b.state = 'down' + + # The bonding mode can not be changed when there are interfaces enslaved + # to this bond, thus we will free all interfaces from the bond first! + for intf in b.get_slaves(): + b.del_port(intf) + + # ARP link monitoring frequency + b.arp_interval = bond['arp_mon_intvl'] + # reset miimon on arp-montior deletion + if bond['arp_mon_intvl'] == 0: + # reset miimon to default + b.bond_miimon = 250 + + # ARP monitor targets need to be synchronized between sysfs and CLI. + # Unfortunately an address can't be send twice to sysfs as this will + # result in the following exception: OSError: [Errno 22] Invalid argument. + # + # We remove ALL adresses prior adding new ones, this will remove addresses + # added manually by the user too - but as we are limited to 16 adresses + # from the kernel side this looks valid to me. We won't run into an error + # when a user added manual adresses which would result in having more + # then 16 adresses in total. + arp_tgt_addr = list(map(str, b.arp_ip_target.split())) + for addr in arp_tgt_addr: + b.arp_ip_target = '-' + addr + + # Add configured ARP target addresses + for addr in bond['arp_mon_tgt']: + b.arp_ip_target = '+' + addr + + # update interface description used e.g. within SNMP + b.ifalias = bond['description'] + + # + # missing DHCP/DHCPv6 options go here + # + + # ignore link state changes + b.link_detect = bond['disable_link_detect'] + # Bonding transmit hash policy + b.xmit_hash_policy = bond['hash_policy'] + # configure ARP cache timeout in milliseconds + b.arp_cache_tmp = bond['ip_arp_cache_tmo'] + # Enable proxy-arp on this interface + b.proxy_arp = bond['ip_proxy_arp'] + # Enable private VLAN proxy ARP on this interface + b.proxy_arp_pvlan = bond['ip_proxy_arp_pvlan'] + + # Change interface MAC address + if bond['mac']: + b.mac = bond['mac'] + + # Bonding policy + b.mode = bond['mode'] + # Maximum Transmission Unit (MTU) + b.mtu = bond['mtu'] + + # Primary device interface + if bond['primary']: + b.primary = bond['primary'] + + # Add (enslave) interfaces to bond + for intf in bond['member']: + b.add_port(intf) + + # As the bond interface is always disabled first when changing + # parameters we will only re-enable the interface if it is not + # administratively disabled + if not bond['disable']: + b.state = 'up' + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in bond['address_remove']: + b.del_addr(addr) + for addr in bond['address']: + b.add_addr(addr) + + # remove no longer required service VLAN interfaces (vif-s) + for vif_s in bond['vif_s_remove']: + b.del_vlan(vif_s) + + # create service VLAN interfaces (vif-s) + for vif_s in bond['vif_s']: + s_vlan = b.add_vlan(vif_s['id'], ethertype=vif_s['ethertype']) + apply_vlan_config(s_vlan, vif_s) + + # remove no longer required client VLAN interfaces (vif-c) + # on lower service VLAN interface + for vif_c in vif_s['vif_c_remove']: + s_vlan.del_vlan(vif_c) + + # create client VLAN interfaces (vif-c) + # on lower service VLAN interface + for vif_c in vif_s['vif_c']: + c_vlan = s_vlan.add_vlan(vif_c['id']) + apply_vlan_config(c_vlan, vif_c) + + # remove no longer required VLAN interfaces (vif) + for vif in bond['vif_remove']: + b.del_vlan(vif) + + # create VLAN interfaces (vif) + for vif in bond['vif']: + vlan = b.add_vlan(vif['id']) + apply_vlan_config(vlan, vif) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 543349e7b..401182a0d 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -17,51 +17,39 @@ # import os -import sys -import copy -import subprocess -import vyos.configinterface as VyIfconfig +from copy import deepcopy +from sys import exit +from netifaces import interfaces +from vyos.ifconfig import BridgeIf, Interface +from vyos.configdict import list_diff from vyos.config import Config from vyos import ConfigError default_config_data = { 'address': [], 'address_remove': [], - 'aging': '300', - 'arp_cache_timeout_ms': '30000', + 'aging': 300, + 'arp_cache_tmo': 30, 'description': '', 'deleted': False, - 'dhcp_client_id': '', - 'dhcp_hostname': '', - 'dhcpv6_parameters_only': False, - 'dhcpv6_temporary': False, 'disable': False, - 'disable_link_detect': False, - 'forwarding_delay': '15', - 'hello_time': '2', + 'disable_link_detect': 1, + 'forwarding_delay': 14, + 'hello_time': 2, 'igmp_querier': 0, 'intf': '', 'mac' : '', - 'max_age': '20', + 'max_age': 20, 'member': [], 'member_remove': [], - 'priority': '32768', - 'stp': 'off' + 'priority': 32768, + 'stp': 0 } -def subprocess_cmd(command): - process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True) - proc_stdout = process.communicate()[0].strip() - pass - -def diff(first, second): - second = set(second) - return [item for item in first if item not in second] - def get_config(): - bridge = copy.deepcopy(default_config_data) + bridge = deepcopy(default_config_data) conf = Config() # determine tagNode instance @@ -82,45 +70,34 @@ def get_config(): if conf.exists('address'): bridge['address'] = conf.return_values('address') + # Determine interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed + eff_addr = conf.return_effective_values('address') + bridge['address_remove'] = list_diff(eff_addr, bridge['address']) + # retrieve aging - how long addresses are retained if conf.exists('aging'): - bridge['aging'] = conf.return_value('aging') + bridge['aging'] = int(conf.return_value('aging')) # retrieve interface description if conf.exists('description'): bridge['description'] = conf.return_value('description') - # DHCP client identifier - if conf.exists('dhcp-options client-id'): - bridge['dhcp_client_id'] = conf.return_value('dhcp-options client-id') - - # DHCP client hostname - if conf.exists('dhcp-options host-name'): - bridge['dhcp_hostname'] = conf.return_value('dhcp-options host-name') - - # DHCPv6 acquire only config parameters, no address - if conf.exists('dhcpv6-options parameters-only'): - bridge['dhcpv6_parameters_only'] = True - - # DHCPv6 IPv6 "temporary" address - if conf.exists('dhcpv6-options temporary'): - bridge['dhcpv6_temporary'] = True - # Disable this bridge interface if conf.exists('disable'): bridge['disable'] = True # Ignore link state changes if conf.exists('disable-link-detect'): - bridge['disable_link_detect'] = True + bridge['disable_link_detect'] = 2 # Forwarding delay if conf.exists('forwarding-delay'): - bridge['forwarding_delay'] = conf.return_value('forwarding-delay') + bridge['forwarding_delay'] = int(conf.return_value('forwarding-delay')) # Hello packet advertisment interval if conf.exists('hello-time'): - bridge['hello_time'] = conf.return_value('hello-time') + bridge['hello_time'] = int(conf.return_value('hello-time')) # Enable Internet Group Management Protocol (IGMP) querier if conf.exists('igmp querier'): @@ -128,8 +105,7 @@ def get_config(): # ARP cache entry timeout in seconds if conf.exists('ip arp-cache-timeout'): - tmp = 1000 * int(conf.return_value('ip arp-cache-timeout')) - bridge['arp_cache_timeout_ms'] = str(tmp) + bridge['arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) # Media Access Control (MAC) address if conf.exists('mac'): @@ -137,21 +113,24 @@ def get_config(): # Interval at which neighbor bridges are removed if conf.exists('max-age'): - bridge['max_age'] = conf.return_value('max-age') + bridge['max_age'] = int(conf.return_value('max-age')) # Determine bridge member interface (currently configured) for intf in conf.list_nodes('member interface'): + # cost and priority initialized with linux defaults + # by reading /sys/devices/virtual/net/br0/brif/eth2/{path_cost,priority} + # after adding interface to bridge after reboot iface = { 'name': intf, - 'cost': '', - 'priority': '' + 'cost': 100, + 'priority': 32 } if conf.exists('member interface {} cost'.format(intf)): - iface['cost'] = conf.return_value('member interface {} cost'.format(intf)) + iface['cost'] = int(conf.return_value('member interface {} cost'.format(intf))) if conf.exists('member interface {} priority'.format(intf)): - iface['priority'] = conf.return_value('member interface {} priority'.format(intf)) + iface['priority'] = int(conf.return_value('member interface {} priority'.format(intf))) bridge['member'].append(iface) @@ -159,28 +138,19 @@ def get_config(): # interfaces is no longer assigend to the bridge and thus can be removed eff_intf = conf.list_effective_nodes('member interface') act_intf = conf.list_nodes('member interface') - bridge['member_remove'] = diff(eff_intf, act_intf) - - # Determine interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed from the bridge - eff_addr = conf.return_effective_values('address') - act_addr = conf.return_values('address') - bridge['address_remove'] = diff(eff_addr, act_addr) + bridge['member_remove'] = list_diff(eff_intf, act_intf) # Priority for this bridge if conf.exists('priority'): - bridge['priority'] = conf.return_value('priority') + bridge['priority'] = int(conf.return_value('priority')) # Enable spanning tree protocol if conf.exists('stp'): - bridge['stp'] = 'on' + bridge['stp'] = 1 return bridge def verify(bridge): - if bridge is None: - return None - conf = Config() for br in conf.list_nodes('interfaces bridge'): # it makes no sense to verify ourself in this case @@ -190,108 +160,88 @@ def verify(bridge): for intf in bridge['member']: tmp = conf.list_nodes('interfaces bridge {} member interface'.format(br)) if intf['name'] in tmp: - raise ConfigError('{} can be assigned to any one bridge only'.format(intf['name'])) + raise ConfigError('Interface "{}" belongs to bridge "{}" and can not be enslaved.'.format(intf['name'], bridge['intf'])) + + # the interface must exist prior adding it to a bridge + for intf in bridge['member']: + if intf['name'] not in interfaces(): + raise ConfigError('Can not add non existing interface "{}" to bridge "{}"'.format(intf['name'], bridge['intf'])) + + # bridge members are not allowed to be bond members, too + for intf in bridge['member']: + for bond in conf.list_nodes('interfaces bonding'): + if conf.exists('interfaces bonding ' + bond + ' member interface'): + if intf['name'] in conf.return_values('interfaces bonding ' + bond + ' member interface'): + raise ConfigError('Interface {} belongs to bond {}, can not add it to {}'.format(intf['name'], bond, bridge['intf'])) return None def generate(bridge): - if bridge is None: - return None - return None def apply(bridge): - if bridge is None: - return None + br = BridgeIf(bridge['intf']) - cmd = '' if bridge['deleted']: - # bridges need to be shutdown first - cmd += 'ip link set dev "{}" down'.format(bridge['intf']) - cmd += ' && ' - # delete bridge - cmd += 'brctl delbr "{}"'.format(bridge['intf']) - subprocess_cmd(cmd) - + # delete bridge interface + # DHCP is stopped inside remove() + br.remove() else: - # create bridge if it does not exist - if not os.path.exists("/sys/class/net/" + bridge['intf']): - # create bridge interface - cmd += 'brctl addbr "{}"'.format(bridge['intf']) - cmd += ' && ' - # activate "UP" the interface - cmd += 'ip link set dev "{}" up'.format(bridge['intf']) - cmd += ' && ' - + # enable interface + br.state = 'up' # set ageing time - cmd += 'brctl setageing "{}" "{}"'.format(bridge['intf'], bridge['aging']) - cmd += ' && ' - + br.ageing_time = bridge['aging'] # set bridge forward delay - cmd += 'brctl setfd "{}" "{}"'.format(bridge['intf'], bridge['forwarding_delay']) - cmd += ' && ' - + br.forward_delay = bridge['forwarding_delay'] # set hello time - cmd += 'brctl sethello "{}" "{}"'.format(bridge['intf'], bridge['hello_time']) - cmd += ' && ' - + br.hello_time = bridge['hello_time'] # set max message age - cmd += 'brctl setmaxage "{}" "{}"'.format(bridge['intf'], bridge['max_age']) - cmd += ' && ' - + br.max_age = bridge['max_age'] # set bridge priority - cmd += 'brctl setbridgeprio "{}" "{}"'.format(bridge['intf'], bridge['priority']) - cmd += ' && ' - + br.priority = bridge['priority'] # turn stp on/off - cmd += 'brctl stp "{}" "{}"'.format(bridge['intf'], bridge['stp']) - - for intf in bridge['member_remove']: - # remove interface from bridge - cmd += ' && ' - cmd += 'brctl delif "{}" "{}"'.format(bridge['intf'], intf) - - for intf in bridge['member']: - # add interface to bridge - # but only if it is not yet member of this bridge - if not os.path.exists('/sys/devices/virtual/net/' + bridge['intf'] + '/brif/' + intf['name']): - cmd += ' && ' - cmd += 'brctl addif "{}" "{}"'.format(bridge['intf'], intf['name']) - - # set bridge port cost - if intf['cost']: - cmd += ' && ' - cmd += 'brctl setpathcost "{}" "{}" "{}"'.format(bridge['intf'], intf['name'], intf['cost']) - - # set bridge port priority - if intf['priority']: - cmd += ' && ' - cmd += 'brctl setportprio "{}" "{}" "{}"'.format(bridge['intf'], intf['name'], intf['priority']) - - subprocess_cmd(cmd) + br.stp_state = bridge['stp'] + # enable or disable IGMP querier + br.multicast_querier = bridge['igmp_querier'] + # update interface description used e.g. within SNMP + br.ifalias = bridge['description'] # Change interface MAC address if bridge['mac']: - VyIfconfig.set_mac_address(bridge['intf'], bridge['mac']) - - # update interface description used e.g. within SNMP - VyIfconfig.set_description(bridge['intf'], bridge['description']) + br.mac = bridge['mac'] - # Ignore link state changes? - VyIfconfig.set_link_detect(bridge['intf'], bridge['disable_link_detect']) + # remove interface from bridge + for intf in bridge['member_remove']: + br.del_port( intf['name'] ) - # enable or disable IGMP querier - VyIfconfig.set_multicast_querier(bridge['intf'], bridge['igmp_querier']) + # add interfaces to bridge + for member in bridge['member']: + br.add_port(member['name']) - # ARP cache entry timeout in seconds - VyIfconfig.set_arp_cache_timeout(bridge['intf'], bridge['arp_cache_timeout_ms']) + # up/down interface + if bridge['disable']: + br.state = 'down' # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second for addr in bridge['address_remove']: - VyIfconfig.remove_interface_address(bridge['intf'], addr) - + br.del_addr(addr) for addr in bridge['address']: - VyIfconfig.add_interface_address(bridge['intf'], addr) + br.add_addr(addr) + + # configure additional bridge member options + for member in bridge['member']: + # set bridge port cost + br.set_cost(member['name'], member['cost']) + # set bridge port priority + br.set_priority(member['name'], member['priority']) + + i = Interface(member['name']) + # configure ARP cache timeout + i.arp_cache_tmo = bridge['arp_cache_tmo'] + # ignore link state changes + i.link_detect = bridge['disable_link_detect'] return None @@ -303,4 +253,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py new file mode 100755 index 000000000..614fe08db --- /dev/null +++ b/src/conf_mode/interface-dummy.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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/>. +# +# + +from os import environ +from copy import deepcopy +from sys import exit + +from vyos.ifconfig import DummyIf +from vyos.configdict import list_diff +from vyos.config import Config +from vyos import ConfigError + +default_config_data = { + 'address': [], + 'address_remove': [], + 'deleted': False, + 'description': '', + 'disable': False, + 'intf': '' +} + +def get_config(): + dummy = deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + dummy['intf'] = environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + # Check if interface has been removed + if not conf.exists('interfaces dummy ' + dummy['intf']): + dummy['deleted'] = True + return dummy + + # set new configuration level + conf.set_level('interfaces dummy ' + dummy['intf']) + + # retrieve configured interface addresses + if conf.exists('address'): + dummy['address'] = conf.return_values('address') + + # retrieve interface description + if conf.exists('description'): + dummy['description'] = conf.return_value('description') + + # Disable this interface + if conf.exists('disable'): + dummy['disable'] = True + + # Determine interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed from the interface + eff_addr = conf.return_effective_values('address') + act_addr = conf.return_values('address') + dummy['address_remove'] = list_diff(eff_addr, act_addr) + + return dummy + +def verify(dummy): + return None + +def generate(dummy): + return None + +def apply(dummy): + du = DummyIf(dummy['intf']) + + # Remove dummy interface + if dummy['deleted']: + du.remove() + else: + # enable interface + du.state = 'up' + # update interface description used e.g. within SNMP + du.ifalias = dummy['description'] + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in dummy['address_remove']: + du.del_addr(addr) + for addr in dummy['address']: + du.add_addr(addr) + + # disable interface on demand + if dummy['disable']: + du.state = 'down' + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interface-loopback.py b/src/conf_mode/interface-loopback.py new file mode 100755 index 000000000..a1a807868 --- /dev/null +++ b/src/conf_mode/interface-loopback.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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/>. +# + +from os import environ +from sys import exit +from copy import deepcopy + +from vyos.ifconfig import LoopbackIf +from vyos.configdict import list_diff +from vyos.config import Config +from vyos import ConfigError + +default_config_data = { + 'address': [], + 'address_remove': [], + 'deleted': False, + 'description': '', +} + + +def get_config(): + loopback = deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + loopback['intf'] = environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + # Check if interface has been removed + if not conf.exists('interfaces loopback ' + loopback['intf']): + loopback['deleted'] = True + + # set new configuration level + conf.set_level('interfaces loopback ' + loopback['intf']) + + # retrieve configured interface addresses + if conf.exists('address'): + loopback['address'] = conf.return_values('address') + + # retrieve interface description + if conf.exists('description'): + loopback['description'] = conf.return_value('description') + + # Determine interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed from the interface + eff_addr = conf.return_effective_values('address') + act_addr = conf.return_values('address') + loopback['address_remove'] = list_diff(eff_addr, act_addr) + + return loopback + +def verify(loopback): + return None + +def generate(loopback): + return None + +def apply(loopback): + lo = LoopbackIf(loopback['intf']) + if not loopback['deleted']: + # update interface description used e.g. within SNMP + # update interface description used e.g. within SNMP + lo.ifalias = loopback['description'] + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in loopback['address']: + lo.add_addr(addr) + + # remove interface address(es) + for addr in loopback['address_remove']: + lo.del_addr(addr) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index 4e5915d4e..548c78535 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -18,24 +18,25 @@ import os import re -import pwd -import grp import sys import stat -import copy import jinja2 -import psutil -from ipaddress import ip_address,ip_network,IPv4Interface -from signal import SIGUSR1 +from copy import deepcopy +from grp import getgrnam +from ipaddress import ip_address,ip_network,IPv4Interface +from netifaces import interfaces +from psutil import pid_exists +from pwd import getpwnam from subprocess import Popen, PIPE +from time import sleep from vyos.config import Config from vyos import ConfigError from vyos.validate import is_addr_assigned -user = 'nobody' -group = 'nogroup' +user = 'openvpn' +group = 'openvpn' # Please be careful if you edit the template. config_tmpl = """ @@ -58,6 +59,7 @@ dev {{ intf }} user {{ uid }} group {{ gid }} persist-key +iproute /usr/libexec/vyos/system/unpriv-ip proto {% if 'tcp-active' in protocol -%}tcp-client{% elif 'tcp-passive' in protocol -%}tcp-server{% else %}udp{% endif %} @@ -301,8 +303,8 @@ def openvpn_mkdir(directory): # fix permissions - corresponds to mode 755 os.chmod(directory, stat.S_IRWXU|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH) - uid = pwd.getpwnam(user).pw_uid - gid = grp.getgrnam(group).gr_gid + uid = getpwnam(user).pw_uid + gid = getgrnam(group).gr_gid os.chown(directory, uid, gid) def fixup_permission(filename, permission=stat.S_IRUSR): @@ -314,8 +316,8 @@ def fixup_permission(filename, permission=stat.S_IRUSR): os.chmod(filename, permission) # make file owned by root / vyattacfg - uid = pwd.getpwnam('root').pw_uid - gid = grp.getgrnam('vyattacfg').gr_gid + uid = getpwnam('root').pw_uid + gid = getgrnam('vyattacfg').gr_gid os.chown(filename, uid, gid) def checkCertHeader(header, filename): @@ -334,7 +336,7 @@ def checkCertHeader(header, filename): return False def get_config(): - openvpn = copy.deepcopy(default_config_data) + openvpn = deepcopy(default_config_data) conf = Config() # determine tagNode instance @@ -792,8 +794,8 @@ def generate(openvpn): fixup_permission(auth_file) # get numeric uid/gid - uid = pwd.getpwnam(user).pw_uid - gid = grp.getgrnam(group).gr_gid + uid = getpwnam(user).pw_uid + gid = getgrnam(group).gr_gid # Generate client specific configuration for client in openvpn['client']: @@ -806,6 +808,11 @@ def generate(openvpn): tmpl = jinja2.Template(config_tmpl) config_text = tmpl.render(openvpn) + + # we need to support quoting of raw parameters from OpenVPN CLI + # see https://phabricator.vyos.net/T1632 + config_text = config_text.replace(""",'"') + with open(get_config_name(interface), 'w') as f: f.write(config_text) os.chown(get_config_name(interface), uid, gid) @@ -813,47 +820,46 @@ def generate(openvpn): return None def apply(openvpn): - interface = openvpn['intf'] - pid = 0 - pidfile = '/var/run/openvpn/{}.pid'.format(interface) + pidfile = '/var/run/openvpn/{}.pid'.format(openvpn['intf']) if os.path.isfile(pidfile): pid = 0 with open(pidfile, 'r') as f: pid = int(f.read()) - # If tunnel interface has been deleted - stop service - if openvpn['deleted'] or openvpn['disable']: - directory = os.path.dirname(get_config_name(interface)) + # Always stop OpenVPN service. We can not send a SIGUSR1 for restart of the + # service as the configuration is not re-read. Stop daemon only if it's + # running - it could have died or killed by someone evil + if pid_exists(pid): + cmd = 'start-stop-daemon --stop --quiet' + cmd += ' --pidfile ' + pidfile + subprocess_cmd(cmd) - # we only need to stop the demon if it's running - # daemon could have died or killed by someone - if psutil.pid_exists(pid): - cmd = 'start-stop-daemon --stop --quiet' - cmd += ' --pidfile ' + pidfile - subprocess_cmd(cmd) - - # cleanup old PID file - if os.path.isfile(pidfile): - os.remove(pidfile) + # cleanup old PID file + if os.path.isfile(pidfile): + os.remove(pidfile) + # Do some cleanup when OpenVPN is disabled/deleted + if openvpn['deleted'] or openvpn['disable']: # cleanup old configuration file - if os.path.isfile(get_config_name(interface)): - os.remove(get_config_name(interface)) + if os.path.isfile(get_config_name(openvpn['intf'])): + os.remove(get_config_name(openvpn['intf'])) # cleanup client config dir - if os.path.isdir(directory + '/ccd/' + interface): + directory = os.path.dirname(get_config_name(openvpn['intf'])) + if os.path.isdir(directory + '/ccd/' + openvpn['intf']): try: - os.remove(directory + '/ccd/' + interface + '/*') + os.remove(directory + '/ccd/' + openvpn['intf'] + '/*') except: pass return None - # Send SIGUSR1 to the process instead of creating a new process - if psutil.pid_exists(pid): - os.kill(pid, SIGUSR1) - return None + # On configuration change we need to wait for the 'old' interface to + # vanish from the Kernel, if it is not gone, OpenVPN will report: + # ERROR: Cannot ioctl TUNSETIFF vtun10: Device or resource busy (errno=16) + while openvpn['intf'] in interfaces(): + sleep(0.250) # 250ms # No matching OpenVPN process running - maybe it got killed or none # existed - nevertheless, spawn new OpenVPN process @@ -862,13 +868,13 @@ def apply(openvpn): cmd += ' --exec /usr/sbin/openvpn' # now pass arguments to openvpn binary cmd += ' --' - cmd += ' --config ' + get_config_name(interface) + cmd += ' --config ' + get_config_name(openvpn['intf']) # execute assembled command subprocess_cmd(cmd) - return None + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/interface-vxlan.py b/src/conf_mode/interface-vxlan.py new file mode 100755 index 000000000..59022238e --- /dev/null +++ b/src/conf_mode/interface-vxlan.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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/>. +# + +from os import environ +from sys import exit +from copy import deepcopy + +from vyos.configdict import list_diff +from vyos.config import Config +from vyos.ifconfig import VXLANIf, Interface +from vyos.interfaces import get_type_of_interface +from vyos import ConfigError +from netifaces import interfaces + +default_config_data = { + 'address': [], + 'address_remove': [], + 'deleted': False, + 'description': '', + 'disable': False, + 'group': '', + 'intf': '', + 'ip_arp_cache_tmo': 30, + 'ip_proxy_arp': 0, + 'link': '', + 'mtu': 1450, + 'remote': '', + 'remote_port': 8472 # The Linux implementation of VXLAN pre-dates + # the IANA's selection of a standard destination port +} + + +def get_config(): + vxlan = deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + vxlan['intf'] = environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + # Check if interface has been removed + if not conf.exists('interfaces vxlan ' + vxlan['intf']): + vxlan['deleted'] = True + return vxlan + + # set new configuration level + conf.set_level('interfaces vxlan ' + vxlan['intf']) + + # retrieve configured interface addresses + if conf.exists('address'): + vxlan['address'] = conf.return_values('address') + + # Determine interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed from the interface + eff_addr = conf.return_effective_values('address') + act_addr = conf.return_values('address') + vxlan['address_remove'] = list_diff(eff_addr, act_addr) + + # retrieve interface description + if conf.exists('description'): + vxlan['description'] = conf.return_value('description') + + # Disable this interface + if conf.exists('disable'): + vxlan['disable'] = True + + # VXLAN multicast grou + if conf.exists('group'): + vxlan['group'] = conf.return_value('group') + + # ARP cache entry timeout in seconds + if conf.exists('ip arp-cache-timeout'): + vxlan['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) + + # Enable proxy-arp on this interface + if conf.exists('ip enable-proxy-arp'): + vxlan['ip_proxy_arp'] = 1 + + # VXLAN underlay interface + if conf.exists('link'): + vxlan['link'] = conf.return_value('link') + + # Maximum Transmission Unit (MTU) + if conf.exists('mtu'): + vxlan['mtu'] = int(conf.return_value('mtu')) + + # Remote address of VXLAN tunnel + if conf.exists('remote'): + vxlan['remote'] = conf.return_value('remote') + + # Remote port of VXLAN tunnel + if conf.exists('port'): + vxlan['remote_port'] = int(conf.return_value('port')) + + # Virtual Network Identifier + if conf.exists('vni'): + vxlan['vni'] = conf.return_value('vni') + + return vxlan + + +def verify(vxlan): + if vxlan['deleted']: + # bail out early + return None + + if vxlan['mtu'] < 1500: + print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU') + + if vxlan['group'] and not vxlan['link']: + raise ConfigError('Multicast VXLAN requires an underlaying interface ') + + if not (vxlan['group'] or vxlan['remote']): + raise ConfigError('Group or remote must be configured') + + if not vxlan['vni']: + raise ConfigError('Must configure VNI for VXLAN') + + if vxlan['link']: + # VXLAN adds a 50 byte overhead - we need to check the underlaying MTU + # if our configured MTU is at least 50 bytes less + underlay_mtu = int(Interface(vxlan['link']).mtu) + if underlay_mtu < (vxlan['mtu'] + 50): + raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \ + 'MTU is to small ({})'.format(underlay_mtu)) + + return None + + +def generate(vxlan): + return None + + +def apply(vxlan): + # Check if the VXLAN interface already exists + if vxlan['intf'] in interfaces(): + v = VXLANIf(vxlan['intf']) + # VXLAN is super picky and the tunnel always needs to be recreated, + # thus we can simply always delete it first. + v.remove() + + if not vxlan['deleted']: + # VXLAN interface needs to be created on-block + # instead of passing a ton of arguments, I just use a dict + # that is managed by vyos.ifconfig + conf = deepcopy(VXLANIf.get_config()) + + # Assign VXLAN instance configuration parameters to config dict + conf['vni'] = vxlan['vni'] + conf['group'] = vxlan['group'] + conf['dev'] = vxlan['link'] + conf['remote'] = vxlan['remote'] + conf['port'] = vxlan['remote_port'] + + # Finally create the new interface + v = VXLANIf(vxlan['intf'], config=conf) + # update interface description used e.g. by SNMP + v.ifalias = vxlan['description'] + # Maximum Transfer Unit (MTU) + v.mtu = vxlan['mtu'] + + # configure ARP cache timeout in milliseconds + v.arp_cache_tmp = vxlan['ip_arp_cache_tmo'] + # Enable proxy-arp on this interface + v.proxy_arp = vxlan['ip_proxy_arp'] + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in vxlan['address_remove']: + v.del_addr(addr) + for addr in vxlan['address']: + v.add_addr(addr) + + # As the bond interface is always disabled first when changing + # parameters we will only re-enable the interface if it is not + # administratively disabled + if not vxlan['disable']: + v.state='up' + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interface-wireguard.py b/src/conf_mode/interface-wireguard.py index 8234fad0b..d51a7a08d 100755 --- a/src/conf_mode/interface-wireguard.py +++ b/src/conf_mode/interface-wireguard.py @@ -24,350 +24,243 @@ import subprocess from vyos.config import Config from vyos import ConfigError +from vyos.ifconfig import WireGuardIf -dir = r'/config/auth/wireguard' -pk = dir + '/private.key' -pub = dir + '/public.key' -psk_file = r'/tmp/psk' +ifname = str(os.environ['VYOS_TAGNODE_VALUE']) +intfc = WireGuardIf(ifname) + +kdir = r'/config/auth/wireguard' def check_kmod(): - if not os.path.exists('/sys/module/wireguard'): - 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") + if not os.path.exists('/sys/module/wireguard'): + 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(): - c = Config() - if not c.exists('interfaces wireguard'): - return None - - 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)) - - 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', - 'fwmark' : 0x00, - 'mtu' : '1420', - 'peer' : {} - } + c = Config() + if not c.exists('interfaces wireguard'): + return None + + config_data = { + ifname: { + 'addr': '', + 'descr': ifname, + 'lport': None, + 'status': 'exists', + 'state': 'enabled', + 'fwmark': 0x00, + 'mtu': 1420, + 'peer': {}, + 'pk' : '{}/default/private.key'.format(kdir) } - ) - - ### determine status either delete or create - for i in new_lst: - config_data['interfaces'][i]['status'] = 'create' - - for i in del_lst: - config_data['interfaces'].update( - { - i : { - 'status': 'delete' - } - } - ) - - ### based on the status, setup conf values - for intfc in intfcs: - cnf = 'wireguard ' + intfc - if config_data['interfaces'][intfc]['status'] != 'delete': - ### addresses - if c.exists(cnf + ' address'): - config_data['interfaces'][intfc]['addr'] = c.return_values(cnf + ' address') - ### interface up/down - if c.exists(cnf + ' disable'): - config_data['interfaces'][intfc]['state'] = 'disable' - ### listen port - if c.exists(cnf + ' port'): - config_data['interfaces'][intfc]['lport'] = c.return_value(cnf + ' port') - ### fwmark - if c.exists(cnf + ' fwmark'): - config_data['interfaces'][intfc]['fwmark'] = c.return_value(cnf + ' fwmark') - ### 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'): - if not c.exists(cnf + ' peer ' + p + ' disable'): - 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') - - return config_data + } + + c.set_level('interfaces wireguard') + if not c.exists_effective(ifname): + config_data[ifname]['status'] = 'create' + + if not c.exists(ifname) and c.exists_effective(ifname): + config_data[ifname]['status'] = 'delete' + + if config_data[ifname]['status'] != 'delete': + if c.exists(ifname + ' address'): + config_data[ifname]['addr'] = c.return_values(ifname + ' address') + if c.exists(ifname + ' disable'): + config_data[ifname]['state'] = 'disable' + if c.exists(ifname + ' port'): + config_data[ifname]['lport'] = c.return_value(ifname + ' port') + if c.exists(ifname + ' fwmark'): + config_data[ifname]['fwmark'] = c.return_value(ifname + ' fwmark') + if c.exists(ifname + ' description'): + config_data[ifname]['descr'] = c.return_value( + ifname + ' description') + if c.exists(ifname + ' mtu'): + config_data[ifname]['mtu'] = c.return_value(ifname + ' mtu') + if c.exists(ifname + ' private-key'): + config_data[ifname]['pk'] = "{0}/{1}/private.key".format(kdir,c.return_value(ifname + ' private-key')) + if c.exists(ifname + ' peer'): + for p in c.list_nodes(ifname + ' peer'): + if not c.exists(ifname + ' peer ' + p + ' disable'): + config_data[ifname]['peer'].update( + { + p: { + 'allowed-ips': [], + 'endpoint': '', + 'pubkey': '' + } + } + ) + if c.exists(ifname + ' peer ' + p + ' pubkey'): + config_data[ifname]['peer'][p]['pubkey'] = c.return_value( + ifname + ' peer ' + p + ' pubkey') + if c.exists(ifname + ' peer ' + p + ' allowed-ips'): + config_data[ifname]['peer'][p]['allowed-ips'] = c.return_values( + ifname + ' peer ' + p + ' allowed-ips') + if c.exists(ifname + ' peer ' + p + ' endpoint'): + config_data[ifname]['peer'][p]['endpoint'] = c.return_value( + ifname + ' peer ' + p + ' endpoint') + if c.exists(ifname + ' peer ' + p + ' persistent-keepalive'): + config_data[ifname]['peer'][p]['persistent-keepalive'] = c.return_value( + ifname + ' peer ' + p + ' persistent-keepalive') + if c.exists(ifname + ' peer ' + p + ' preshared-key'): + config_data[ifname]['peer'][p]['psk'] = c.return_value( + ifname + ' peer ' + p + ' preshared-key') + + return config_data def verify(c): - if not c: - return None + if not c: + return None - 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]['peer']: - raise ConfigError("peer required on interface " + i) + if not os.path.exists(c[ifname]['pk']): + raise ConfigError( + "No keys found, generate them by executing: \'run generate wireguard [keypair|named-keypairs]\'") - 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) + if c[ifname]['status'] != 'delete': + if not c[ifname]['addr']: + raise ConfigError("ERROR: IP address required") + if not c[ifname]['peer']: + raise ConfigError("ERROR: peer required") + for p in c[ifname]['peer']: + if not c[ifname]['peer'][p]['allowed-ips']: + raise ConfigError("ERROR: allowed-ips required for peer " + p) + if not c[ifname]['peer'][p]['pubkey']: + raise ConfigError("peer pubkey required 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: - if os.path.isdir('/sys/class/net/' + dev): - 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)) - sl.syslog(sl.LOG_NOTICE, "removing interface " + wg_intf) - subprocess.call(['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True) - return None - - ### - ## find the diffs between effective config an new config - ### - c_eff = Config() - c_eff.set_level('interfaces wireguard') - - ### link status up/down aka interface disable - - for intf in c['interfaces']: - if not c['interfaces'][intf]['status'] == 'delete': - if c['interfaces'][intf]['state'] == 'disable': - sl.syslog(sl.LOG_NOTICE, "disable interface " + intf) - subprocess.call(['ip l s dev ' + intf + ' down ' + ' &>/dev/null'], shell=True) - else: - sl.syslog(sl.LOG_NOTICE, "enable interface " + intf) - subprocess.call(['ip l s dev ' + intf + ' up ' + ' &>/dev/null'], shell=True) - - ### deletion of a specific interface - for intf in c['interfaces']: - if c['interfaces'][intf]['status'] == 'delete': - sl.syslog(sl.LOG_NOTICE, "removing interface " + intf) - subprocess.call(['ip l d dev ' + intf + ' &>/dev/null'], shell=True) - - ### peer deletion - peer_eff = c_eff.list_effective_nodes( intf + ' peer') + # no wg config left, delete all wireguard devices, if any + if not c: + net_devs = os.listdir('/sys/class/net/') + for dev in net_devs: + if os.path.isdir('/sys/class/net/' + dev): + 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)) + sl.syslog(sl.LOG_NOTICE, "removing interface " + wg_intf) + subprocess.call( + ['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True) + return None + + # interface removal + if c[ifname]['status'] == 'delete': + sl.syslog(sl.LOG_NOTICE, "removing interface " + ifname) + intfc.remove() + return None + + c_eff = Config() + c_eff.set_level('interfaces wireguard') + + # interface state + if c[ifname]['state'] == 'disable': + sl.syslog(sl.LOG_NOTICE, "disable interface " + ifname) + intfc.state = 'down' + else: + if not intfc.state == 'up': + sl.syslog(sl.LOG_NOTICE, "enable interface " + ifname) + intfc.state = 'up' + + # IP address + if not c_eff.exists_effective(ifname + ' address'): + for ip in c[ifname]['addr']: + intfc.add_addr(ip) + else: + addr_eff = c_eff.return_effective_values(ifname + ' address') + addr_rem = list(set(addr_eff) - set(c[ifname]['addr'])) + addr_add = list(set(c[ifname]['addr']) - set(addr_eff)) + + if len(addr_rem) != 0: + for ip in addr_rem: + sl.syslog( + sl.LOG_NOTICE, "remove IP address {0} from {1}".format(ip, ifname)) + intfc.del_addr(ip) + + if len(addr_add) != 0: + for ip in addr_add: + sl.syslog( + sl.LOG_NOTICE, "add IP address {0} to {1}".format(ip, ifname)) + intfc.add_addr(ip) + + # interface MTU + if c[ifname]['mtu'] != 1420: + intfc.mtu = int(c[ifname]['mtu']) + else: + # default is set to 1420 in config_data + intfc.mtu = int(c[ifname]['mtu']) + + # ifalias for snmp from description + descr_eff = c_eff.return_effective_value(ifname + ' description') + if descr_eff != c[ifname]['descr']: + intfc.ifalias = str(c[ifname]['descr']) + + # peer deletion + peer_eff = c_eff.list_effective_nodes(ifname + ' peer') peer_cnf = [] + try: - for p in c['interfaces'][intf]['peer']: - peer_cnf.append(p) + for p in c[ifname]['peer']: + peer_cnf.append(p) except KeyError: - pass + pass peer_rem = list(set(peer_eff) - set(peer_cnf)) for p in peer_rem: - pkey = c_eff.return_effective_value( intf + ' peer ' + p +' pubkey') - remove_peer(intf, pkey) + pkey = c_eff.return_effective_value(ifname + ' peer ' + p + ' pubkey') + intfc.remove_peer(pkey) - ### peer pubkey update - ### wg identifies peers by its pubky, so we have to remove the peer first - ### it will recreated it then below with the new key from the cli config + # peer key update for p in peer_eff: - if p in peer_cnf: - ekey = c_eff.return_effective_value( intf + ' peer ' + p +' pubkey') - nkey = c['interfaces'][intf]['peer'][p]['pubkey'] - if nkey != ekey: - sl.syslog(sl.LOG_NOTICE, "peer " + p + ' changed pubkey from ' + ekey + 'to key ' + nkey + ' on interface ' + intf) - remove_peer(intf, ekey) - - ### new config - if c['interfaces'][intf]['status'] == 'create': - if not os.path.exists(pk): - raise ConfigError("No keys found, generate them by executing: \'run generate wireguard keypair\'") - - 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) - - 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 = c_eff.return_effective_values(intf + ' address') - 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: - for addr in addr_rem: - del_addr(intf, addr) - - if len(addr_add) != 0: - for addr in addr_add: - add_addr(intf, addr) - - ## 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['interfaces'][intf]['peer']: - val_eff = "" - val = "" - - try: - val = c['interfaces'][intf]['peer'][p]['persistent-keepalive'] - except KeyError: - pass - - if c_eff.exists_effective(intf + ' peer ' + p + ' persistent-keepalive'): - val_eff = c_eff.return_effective_value(intf + ' peer ' + p + ' persistent-keepalive') - - ### disable keepalive - if val_eff and not val: - 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) - - ### ifalias for snmp from description - if c['interfaces'][intf]['status'] != 'delete': - descr_eff = c_eff.return_effective_value(intf + ' description') - cnf_descr = c['interfaces'][intf]['descr'] - if descr_eff != 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']: - ## 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 - } - - ## 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'] - - ## fwmark - if c['interfaces'][intf]['fwmark']: - wg_config['fwmark'] = c['interfaces'][intf]['fwmark'] - - ## 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 += " fwmark " + str(wg_config['fwmark']) - 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 - - if wg_config['endpoint']: - cmd += " endpoint " + wg_config['endpoint'] - - if wg_config['keepalive'] != 0: - cmd += " persistent-keepalive " + wg_config['keepalive'] - else: - cmd += " persistent-keepalive 0" - - 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): - # see https://phabricator.vyos.net/T949 - ret = subprocess.call(['ip a a dev ' + intf + ' ' + addr + ' &>/dev/null'], shell=True) - 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) - sl.syslog(sl.LOG_NOTICE, "ip a d dev " + intf + " " + addr) - -def remove_peer(intf, peer_key): - cmd = 'sudo wg set ' + str(intf) + ' peer ' + peer_key + ' remove &>/dev/null' - ret = subprocess.call([cmd], shell=True) - sl.syslog(sl.LOG_NOTICE, "peer " + peer_key + " removed from " + intf) + if p in peer_cnf: + ekey = c_eff.return_effective_value( + ifname + ' peer ' + p + ' pubkey') + nkey = c[ifname]['peer'][p]['pubkey'] + if nkey != ekey: + sl.syslog( + sl.LOG_NOTICE, "peer {0} pubkey changed from {1} to {2} on interface {3}".format(p, ekey, nkey, ifname)) + intfc.remove_peer(ekey) + + intfc.config['private-key'] = c[ifname]['pk'] + for p in c[ifname]['peer']: + intfc.config['pubkey'] = str(c[ifname]['peer'][p]['pubkey']) + intfc.config['allowed-ips'] = (c[ifname]['peer'][p]['allowed-ips']) + + # listen-port + if c[ifname]['lport']: + intfc.config['port'] = c[ifname]['lport'] + + # fwmark + if c[ifname]['fwmark']: + intfc.config['fwmark'] = c[ifname]['fwmark'] + + # endpoint + if c[ifname]['peer'][p]['endpoint']: + intfc.config['endpoint'] = c[ifname]['peer'][p]['endpoint'] + + # persistent-keepalive + if 'persistent-keepalive' in c[ifname]['peer'][p]: + intfc.config['keepalive'] = c[ifname][ + 'peer'][p]['persistent-keepalive'] + + # preshared-key - needs to be read from a file + if 'psk' in c[ifname]['peer'][p]: + psk_file = '/config/auth/wireguard/psk' + old_umask = os.umask(0o077) + open(psk_file, 'w').write(str(c[ifname]['peer'][p]['psk'])) + os.umask(old_umask) + intfc.config['psk'] = psk_file + + intfc.update() if __name__ == '__main__': - try: - check_kmod() - c = get_config() - verify(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) + try: + check_kmod() + c = get_config() + verify(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py index 8d25e7abd..156bb2edd 100755 --- a/src/conf_mode/ipsec-settings.py +++ b/src/conf_mode/ipsec-settings.py @@ -62,7 +62,7 @@ conn {{ra_conn_name}} left={{outside_addr}} leftsubnet=%dynamic[/1701] rightsubnet=%dynamic - mark=%unique + mark_in=%unique auto=add ike=aes256-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1024! dpddelay=15 diff --git a/src/conf_mode/syslog.py b/src/conf_mode/syslog.py index 7b79c701b..c4f3d2c9c 100755 --- a/src/conf_mode/syslog.py +++ b/src/conf_mode/syslog.py @@ -24,16 +24,16 @@ import jinja2 from vyos.config import Config from vyos import ConfigError -########### config templates +# config templates -#### /etc/rsyslog.d/vyos-rsyslog.conf ### +# /etc/rsyslog.d/vyos-rsyslog.conf ### configs = ''' ## generated by syslog.py ## ## file based logging {% if files['global']['marker'] -%} $ModLoad immark {% if files['global']['marker-interval'] %} -$MarkMessagePeriod {{files['global']['marker-interval']}} +$MarkMessagePeriod {{files['global']['marker-interval']}} {% endif %} {% endif -%} {% if files['global']['preserver_fqdn'] -%} @@ -80,217 +80,241 @@ logrotate_configs = ''' } {% endfor %} ''' -############# config templates end +# config templates end + def get_config(): - c = Config() - if not c.exists('system syslog'): - return None - c.set_level('system syslog') - - config_data = { - 'files' : {}, - 'console' : {}, - 'hosts' : {}, - 'user' : {} - } - - ##### - # /etc/rsyslog.d/vyos-rsyslog.conf - # 'set system syslog global' - ##### - config_data['files'].update( - { - 'global' : { - 'log-file' : '/var/log/messages', - 'max-size' : 262144, - 'action-on-max-size' : '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog', - 'selectors' : '*.notice;local7.debug', - 'max-files' : '5', - 'preserver_fqdn' : False - } - } - ) - - if c.exists('global marker'): - config_data['files']['global']['marker'] = True - if c.exists('global marker interval'): - config_data['files']['global']['marker-interval'] = c.return_value('global marker interval') - if c.exists('global facility'): - config_data['files']['global']['selectors'] = generate_selectors(c, 'global facility') - if c.exists('global archive size'): - config_data['files']['global']['max-size'] = int(c.return_value('global archive size'))* 1024 - if c.exists('global archive file'): - config_data['files']['global']['max-files'] = c.return_value('global archive file') - if c.exists('global preserve-fqdn'): - config_data['files']['global']['preserver_fqdn'] = True - - ### - # set system syslog file - ### - - if c.exists('file'): - filenames = c.list_nodes('file') - for filename in filenames: - config_data['files'].update( - { - filename : { - 'log-file' : '/var/log/user/' + filename, - 'max-files' : '5', - 'action-on-max-size' : '/usr/sbin/logrotate /etc/logrotate.d/' + filename, - 'selectors' : '*.err', - 'max-size' : 262144 - } - } - ) - - if c.exists('file ' + filename + ' facility'): - config_data['files'][filename]['selectors'] = generate_selectors(c, 'file ' + filename + ' facility') - if c.exists('file ' + filename + ' archive size'): - config_data['files'][filename]['max-size'] = int(c.return_value('file ' + filename + ' archive size'))* 1024 - if c.exists('file ' + filename + ' archive files'): - config_data['files'][filename]['max-files'] = c.return_value('file ' + filename + ' archive files') - - ## set system syslog console - if c.exists('console'): - config_data['console'] = { - '/dev/console' : { - 'selectors' : '*.err' - } + c = Config() + if not c.exists('system syslog'): + return None + c.set_level('system syslog') + + config_data = { + 'files': {}, + 'console': {}, + 'hosts': {}, + 'user': {} } - - for f in c.list_nodes('console facility'): - if c.exists('console facility ' + f + ' level'): - config_data['console'] = { - '/dev/console' : { - 'selectors' : generate_selectors(c, 'console facility') - } - } - - ## set system syslog host - if c.exists('host'): - proto = 'udp' - rhosts = c.list_nodes('host') - for rhost in rhosts: - for fac in c.list_nodes('host ' + rhost + ' facility'): - if c.exists('host ' + rhost + ' facility ' + fac + ' protocol'): - proto = c.return_value('host ' + rhost + ' facility ' + fac + ' protocol') - - config_data['hosts'].update( + + # + # /etc/rsyslog.d/vyos-rsyslog.conf + # 'set system syslog global' + # + config_data['files'].update( { - rhost : { - 'selectors' : generate_selectors(c, 'host ' + rhost + ' facility'), - 'proto' : proto - } + 'global': { + 'log-file': '/var/log/messages', + 'max-size': 262144, + 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog', + 'selectors': '*.notice;local7.debug', + 'max-files': '5', + 'preserver_fqdn': False + } } - ) + ) - ## set system syslog user - if c.exists('user'): - usrs = c.list_nodes('user') - for usr in usrs: - config_data['user'].update( - { - usr : { - 'selectors' : generate_selectors(c, 'user ' + usr + ' facility') - } + if c.exists('global marker'): + config_data['files']['global']['marker'] = True + if c.exists('global marker interval'): + config_data['files']['global'][ + 'marker-interval'] = c.return_value('global marker interval') + if c.exists('global facility'): + config_data['files']['global'][ + 'selectors'] = generate_selectors(c, 'global facility') + if c.exists('global archive size'): + config_data['files']['global']['max-size'] = int( + c.return_value('global archive size')) * 1024 + if c.exists('global archive file'): + config_data['files']['global'][ + 'max-files'] = c.return_value('global archive file') + if c.exists('global preserve-fqdn'): + config_data['files']['global']['preserver_fqdn'] = True + + # + # set system syslog file + # + + if c.exists('file'): + filenames = c.list_nodes('file') + for filename in filenames: + config_data['files'].update( + { + filename: { + 'log-file': '/var/log/user/' + filename, + 'max-files': '5', + 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/' + filename, + 'selectors': '*.err', + 'max-size': 262144 + } + } + ) + + if c.exists('file ' + filename + ' facility'): + config_data['files'][filename]['selectors'] = generate_selectors( + c, 'file ' + filename + ' facility') + if c.exists('file ' + filename + ' archive size'): + config_data['files'][filename]['max-size'] = int( + c.return_value('file ' + filename + ' archive size')) * 1024 + if c.exists('file ' + filename + ' archive files'): + config_data['files'][filename]['max-files'] = c.return_value( + 'file ' + filename + ' archive files') + + # set system syslog console + if c.exists('console'): + config_data['console'] = { + '/dev/console': { + 'selectors': '*.err' + } } - ) - - return config_data + + for f in c.list_nodes('console facility'): + if c.exists('console facility ' + f + ' level'): + config_data['console'] = { + '/dev/console': { + 'selectors': generate_selectors(c, 'console facility') + } + } + + # set system syslog host + if c.exists('host'): + proto = 'udp' + rhosts = c.list_nodes('host') + for rhost in rhosts: + for fac in c.list_nodes('host ' + rhost + ' facility'): + if c.exists('host ' + rhost + ' facility ' + fac + ' protocol'): + proto = c.return_value( + 'host ' + rhost + ' facility ' + fac + ' protocol') + + config_data['hosts'].update( + { + rhost: { + 'selectors': generate_selectors(c, 'host ' + rhost + ' facility'), + 'proto': proto + } + } + ) + + # set system syslog user + if c.exists('user'): + usrs = c.list_nodes('user') + for usr in usrs: + config_data['user'].update( + { + usr: { + 'selectors': generate_selectors(c, 'user ' + usr + ' facility') + } + } + ) + + return config_data + def generate_selectors(c, config_node): -## protocols and security are being mapped here -## for backward compatibility with old configs -## security and protocol mappings can be removed later - if c.is_tag(config_node): - nodes = c.list_nodes(config_node) - selectors = "" - for node in nodes: - lvl = c.return_value( config_node + ' ' + node + ' level') - if lvl == None: - lvl = "err" - if lvl == 'all': - lvl = '*' - if node == 'all' and node != nodes[-1]: - selectors += "*." + lvl + ";" - elif node == 'all': - selectors += "*." + lvl - elif node != nodes[-1]: - if node == 'protocols': - node = 'local7' - if node == 'security': - node = 'auth' - selectors += node + "." + lvl + ";" - else: - if node == 'protocols': - node = 'local7' - if node == 'security': - node = 'auth' - selectors += node + "." + lvl - return selectors +# protocols and security are being mapped here +# for backward compatibility with old configs +# security and protocol mappings can be removed later + if c.is_tag(config_node): + nodes = c.list_nodes(config_node) + selectors = "" + for node in nodes: + lvl = c.return_value(config_node + ' ' + node + ' level') + if lvl == None: + lvl = "err" + if lvl == 'all': + lvl = '*' + if node == 'all' and node != nodes[-1]: + selectors += "*." + lvl + ";" + elif node == 'all': + selectors += "*." + lvl + elif node != nodes[-1]: + if node == 'protocols': + node = 'local7' + if node == 'security': + node = 'auth' + selectors += node + "." + lvl + ";" + else: + if node == 'protocols': + node = 'local7' + if node == 'security': + node = 'auth' + selectors += node + "." + lvl + return selectors + def generate(c): - if c == None: - return None + if c == None: + return None - tmpl = jinja2.Template(configs, trim_blocks=True) - config_text = tmpl.render(c) - with open('/etc/rsyslog.d/vyos-rsyslog.conf', 'w') as f: - f.write(config_text) + tmpl = jinja2.Template(configs, trim_blocks=True) + config_text = tmpl.render(c) + with open('/etc/rsyslog.d/vyos-rsyslog.conf', 'w') as f: + f.write(config_text) + + # eventually write for each file its own logrotate file, since size is + # defined it shouldn't matter + tmpl = jinja2.Template(logrotate_configs, trim_blocks=True) + config_text = tmpl.render(c) + with open('/etc/logrotate.d/vyos-rsyslog', 'w') as f: + f.write(config_text) - ## eventually write for each file its own logrotate file, since size is defined it shouldn't matter - tmpl = jinja2.Template(logrotate_configs, trim_blocks=True) - config_text = tmpl.render(c) - with open('/etc/logrotate.d/vyos-rsyslog', 'w') as f: - f.write(config_text) def verify(c): - if c == None: - return None - # - # /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 - - fac = ['*','auth','authpriv','cron','daemon','kern','lpr','mail','mark','news','protocols','security',\ - 'syslog','user','uucp','local0','local1','local2','local3','local4','local5','local6','local7'] - lvl = ['emerg','alert','crit','err','warning','notice','info','debug','*'] - - for conf in c: - if c[conf]: - for item in c[conf]: - for s in c[conf][item]['selectors'].split(";"): - f = re.sub("\..*$","",s) - if f not in fac: - print (c[conf]) - raise ConfigError('Invalid facility ' + s + ' set in '+ conf + ' ' + item) - l = re.sub("^.+\.","",s) - if l not in lvl: - raise ConfigError('Invalid logging level ' + s + ' set in '+ conf + ' ' + item) + if c == None: + return None + # + # /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 + + fac = [ + '*', 'auth', 'authpriv', 'cron', 'daemon', 'kern', 'lpr', 'mail', 'mark', 'news', 'protocols', 'security', + 'syslog', 'user', 'uucp', 'local0', 'local1', 'local2', 'local3', 'local4', 'local5', 'local6', 'local7'] + lvl = ['emerg', 'alert', 'crit', 'err', + 'warning', 'notice', 'info', 'debug', '*'] + + for conf in c: + if c[conf]: + for item in c[conf]: + for s in c[conf][item]['selectors'].split(";"): + f = re.sub("\..*$", "", s) + if f not in fac: + print (c[conf]) + raise ConfigError( + 'Invalid facility ' + s + ' set in ' + conf + ' ' + item) + l = re.sub("^.+\.", "", s) + if l not in lvl: + raise ConfigError( + 'Invalid logging level ' + s + ' set in ' + conf + ' ' + item) + def apply(c): - if not os.path.exists('/var/run/rsyslogd.pid'): - os.system("sudo systemctl start rsyslog >/dev/null") - else: - os.system("sudo systemctl restart rsyslog >/dev/null") + if not c and os.path.exists('/var/run/rsyslogd.pid'): + os.system("sudo systemctl stop syslog.socket") + os.system("sudo systemctl stop rsyslog") + else: + if not os.path.exists('/var/run/rsyslogd.pid'): + os.system("sudo systemctl start rsyslog >/dev/null") + else: + os.system("sudo systemctl restart rsyslog >/dev/null") if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/helpers/vyos-boot-config-loader.py b/src/helpers/vyos-boot-config-loader.py new file mode 100755 index 000000000..06c95765f --- /dev/null +++ b/src/helpers/vyos-boot-config-loader.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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 os +import sys +import subprocess +import traceback + +from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.configtree import ConfigTree + +STATUS_FILE = '/tmp/vyos-config-status' +TRACE_FILE = '/tmp/boot-config-trace' + +session = ConfigSession(os.getpid(), 'vyos-boot-config-loader') +env = session.get_session_env() + +default_file_name = env['vyatta_sysconfdir'] + '/config.boot.default' + +if len(sys.argv) < 1: + print("Must be called with argument.") + sys.exit(1) +else: + file_name = sys.argv[1] + +def write_config_status(status): + with open(STATUS_FILE, 'w') as f: + f.write('{0}\n'.format(status)) + +def trace_to_file(trace_file_name): + with open(trace_file_name, 'w') as trace_file: + traceback.print_exc(file=trace_file) + +def failsafe(): + try: + with open(default_file_name, 'r') as f: + config_file = f.read() + except Exception as e: + print("Catastrophic: no default config file " + "'{0}'".format(default_file_name)) + sys.exit(1) + + config = ConfigTree(config_file) + if not config.exists(['system', 'login', 'user', 'vyos', + 'authentication', 'encrypted-password']): + print("No password entry in default config file;") + print("unable to recover password for user 'vyos'.") + sys.exit(1) + else: + passwd = config.return_value(['system', 'login', 'user', 'vyos', + 'authentication', + 'encrypted-password']) + + cmd = ("useradd -s /bin/bash -G 'users,sudo' -m -N -p '{0}' " + "vyos".format(passwd)) + try: + subprocess.check_call(cmd, shell=True) + except subprocess.CalledProcessError as e: + sys.exit("{0}".format(e)) + + with open('/etc/motd', 'a+') as f: + f.write('\n\n') + f.write('!!!!!\n') + f.write('There were errors loading the initial configuration;\n') + f.write('please examine the errors in {0} and correct.' + '\n'.format(TRACE_FILE)) + f.write('!!!!!\n\n') + +try: + with open(file_name, 'r') as f: + config_file = f.read() +except Exception as e: + write_config_status(1) + failsafe() + trace_to_file(TRACE_FILE) + sys.exit("{0}".format(e)) + +try: + session.load_config(file_name) + session.commit() + write_config_status(0) +except ConfigSessionError as e: + write_config_status(1) + failsafe() + trace_to_file(TRACE_FILE) + sys.exit(1) diff --git a/src/helpers/vyos-bridge-sync.py b/src/helpers/vyos-bridge-sync.py new file mode 100755 index 000000000..495eb5d40 --- /dev/null +++ b/src/helpers/vyos-bridge-sync.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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/>. +# + +# Script is used to synchronize configured bridge interfaces. +# one can add a non existing interface to a bridge group (e.g. VLAN) +# but the vlan interface itself does yet not exist. It should be added +# to the bridge automatically once it's available + +import argparse +import subprocess + +from sys import exit +from time import sleep +from vyos.config import Config + +def subprocess_cmd(command): + process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True) + proc_stdout = process.communicate()[0].strip() + pass + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-i', '--interface', action='store', help='Interface name which should be added to bridge it is configured for', required=True) + args, unknownargs = parser.parse_known_args() + + conf = Config() + if not conf.list_nodes('interfaces bridge'): + # no bridge interfaces exist .. bail out early + exit(0) + else: + for bridge in conf.list_nodes('interfaces bridge'): + for member_if in conf.list_nodes('interfaces bridge {} member interface'.format(bridge)): + if args.interface == member_if: + cmd = 'brctl addif "{}" "{}"'.format(bridge, args.interface) + # let interfaces etc. settle - especially required for OpenVPN bridged interfaces + sleep(4) + subprocess_cmd(cmd) + + exit(0) diff --git a/src/helpers/vyos-sudo.py b/src/helpers/vyos-sudo.py index 0101a0c95..3e4c196d9 100755 --- a/src/helpers/vyos-sudo.py +++ b/src/helpers/vyos-sudo.py @@ -15,17 +15,10 @@ # 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 getpass -import grp import os import sys - -def is_admin() -> bool: - """Look if current user is in sudo group""" - current_user = getpass.getuser() - (_, _, _, admin_group_members) = grp.getgrnam('sudo') - return current_user in admin_group_members +from vyos.util import is_admin if __name__ == '__main__': diff --git a/src/migration-scripts/dns-forwarding/0-to-1 b/src/migration-scripts/dns-forwarding/0-to-1 new file mode 100755 index 000000000..6e8720eef --- /dev/null +++ b/src/migration-scripts/dns-forwarding/0-to-1 @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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/>. +# + +# This migration script will check if there is a allow-from directive configured +# for the dns forwarding service - if not, the node will be created with the old +# default values of 0.0.0.0/0 and ::/0 + +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) + +base = ['service', 'dns', 'forwarding'] +if not config.exists(base): + # Nothing to do + sys.exit(0) +else: + if not config.exists(base + ['allow-from']): + config.set(base + ['allow-from'], value='0.0.0.0/0', replace=False) + config.set(base + ['allow-from'], value='::/0', replace=False) + + 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/dns-forwarding/1-to-2 b/src/migration-scripts/dns-forwarding/1-to-2 new file mode 100755 index 000000000..31ba5573f --- /dev/null +++ b/src/migration-scripts/dns-forwarding/1-to-2 @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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/>. +# + +# This migration script will remove the deprecated 'listen-on' statement +# from the dns forwarding service and will add the corresponding +# listen-address nodes instead. This is required as PowerDNS can only listen +# on interface addresses and not on interface names. + +import sys + +from ipaddress import ip_interface +from vyos.configtree import ConfigTree +from vyos.interfaces import get_type_of_interface + +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) + +base = ['service', 'dns', 'forwarding'] +if not config.exists(base): + # Nothing to do + sys.exit(0) +else: + if config.exists(base + ['listen-on']): + listen_intf = config.return_values(base + ['listen-on']) + # Delete node with abandoned command + config.delete(base + ['listen-on']) + + # retrieve interface addresses for every configured listen-on interface + listen_addr = [] + for intf in listen_intf: + # we need to treat vif and vif-s interfaces differently, + # both "real interfaces" use dots for vlan identifiers - those + # need to be exchanged with vif and vif-s identifiers + if intf.count('.') == 1: + # this is a regular VLAN interface + intf = intf.split('.')[0] + ' vif ' + intf.split('.')[1] + elif intf.count('.') == 2: + # this is a QinQ VLAN interface + intf = intf.split('.')[0] + ' vif-s ' + intf.split('.')[1] + ' vif-c ' + intf.split('.')[2] + + path = ['interfaces', get_type_of_interface(intf), intf, 'address'] + + # retrieve corresponding interface addresses in CIDR format + # those need to be converted in pure IP addresses without network information + for addr in config.return_values(path): + listen_addr.append( ip_interface(addr).ip ) + + for addr in listen_addr: + config.set(base + ['listen-address'], value=addr, replace=False) + + 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/interfaces/0-to-1 b/src/migration-scripts/interfaces/0-to-1 index 38f2bd8f5..96e18b5d5 100755 --- a/src/migration-scripts/interfaces/0-to-1 +++ b/src/migration-scripts/interfaces/0-to-1 @@ -30,20 +30,22 @@ else: # for br in config.list_nodes(base): # STP: check if enabled - stp_val = config.return_value(base + [br, 'stp']) - # STP: delete node with old syntax - config.delete(base + [br, 'stp']) - # STP: set new node - if enabled - if stp_val == "true": - config.set(base + [br, 'stp'], value=None) + if config.exists(base + [br, 'stp']): + stp_val = config.return_value(base + [br, 'stp']) + # STP: delete node with old syntax + config.delete(base + [br, 'stp']) + # STP: set new node - if enabled + if stp_val == "true": + config.set(base + [br, 'stp'], value=None) # igmp-snooping: check if enabled - igmp_val = config.return_value(base + [br, 'igmp-snooping', 'querier']) - # igmp-snooping: delete node with old syntax - config.delete(base + [br, 'igmp-snooping', 'querier']) - # igmp-snooping: set new node - if enabled - if igmp_val == "enable": - config.set(base + [br, 'igmp', 'querier'], value=None) + if config.exists(base + [br, 'igmp-snooping', 'querier']): + igmp_val = config.return_value(base + [br, 'igmp-snooping', 'querier']) + # igmp-snooping: delete node with old syntax + config.delete(base + [br, 'igmp-snooping', 'querier']) + # igmp-snooping: set new node - if enabled + if igmp_val == "enable": + config.set(base + [br, 'igmp', 'querier'], value=None) # # move interface based bridge-group to actual bridge (de-nest) diff --git a/src/migration-scripts/interfaces/1-to-2 b/src/migration-scripts/interfaces/1-to-2 new file mode 100755 index 000000000..050137318 --- /dev/null +++ b/src/migration-scripts/interfaces/1-to-2 @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 + +# Change syntax of bond interface +# - move interface based bond-group to actual bond (de-nest) +# https://phabricator.vyos.net/T1614 + +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) +base = ['interfaces', 'bonding'] + +if not config.exists(base): + # Nothing to do + sys.exit(0) +else: + # + # move interface based bond-group to actual bond (de-nest) + # + for intf in config.list_nodes(['interfaces', 'ethernet']): + # check if bond-group exists + if config.exists(['interfaces', 'ethernet', intf, 'bond-group']): + # get configured bond interface + bond = config.return_value(['interfaces', 'ethernet', intf, 'bond-group']) + # delete old interface asigned (nested) bond group + config.delete(['interfaces', 'ethernet', intf, 'bond-group']) + # create new bond member interface + config.set(base + [bond, 'member', 'interface'], value=intf, replace=False) + + # + # some combinations were allowed in the past from a CLI perspective + # but the kernel overwrote them - remove from CLI to not confuse the users. + # In addition new consitency checks are in place so users can't repeat the + # mistake. One of those nice issues is https://phabricator.vyos.net/T532 + for bond in config.list_nodes(base): + if config.exists(base + [bond, 'arp-monitor', 'interval']) and config.exists(base + [bond, 'mode']): + mode = config.return_value(base + [bond, 'mode']) + if mode in ['802.3ad', 'transmit-load-balance', 'adaptive-load-balance']: + intvl = int(config.return_value(base + [bond, 'arp-monitor', 'interval'])) + if intvl > 0: + # this is not allowed and the linux kernel replies with: + # option arp_interval: mode dependency failed, not supported in mode 802.3ad(4) + # option arp_interval: mode dependency failed, not supported in mode balance-alb(6) + # option arp_interval: mode dependency failed, not supported in mode balance-tlb(5) + # + # so we simply disable arp_interval by setting it to 0 and miimon will take care about the link + config.set(base + [bond, 'arp-monitor', 'interval'], value='0') + + 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/clear_conntrack.py b/src/op_mode/clear_conntrack.py new file mode 100755 index 000000000..0e52b9086 --- /dev/null +++ b/src/op_mode/clear_conntrack.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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 subprocess +import sys + +from vyos.util import ask_yes_no + +if not ask_yes_no("This will clear all currently tracked and expected connections. Continue?"): + sys.exit(1) +else: + subprocess.check_call(['/usr/sbin/conntrack -F'], shell=True, stderr=subprocess.DEVNULL) + subprocess.check_call(['/usr/sbin/conntrack -F expect'], shell=True, stderr=subprocess.DEVNULL) diff --git a/src/op_mode/format_disk.py b/src/op_mode/format_disk.py new file mode 100755 index 000000000..5a3b250ee --- /dev/null +++ b/src/op_mode/format_disk.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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 argparse +import os +import re +import subprocess +import sys +from datetime import datetime +from time import sleep + +from vyos.util import is_admin, ask_yes_no + + +def list_disks(): + disks = set() + with open('/proc/partitions') as partitions_file: + for line in partitions_file: + fields = line.strip().split() + if len(fields) == 4 and fields[3].isalpha() and fields[3] != 'name': + disks.add(fields[3]) + return disks + + +def is_busy(disk: str): + """Check if given disk device is busy by re-reading it's partition table""" + + cmd = 'sudo blockdev --rereadpt /dev/{}'.format(disk) + status = subprocess.call([cmd], shell=True, stderr=subprocess.DEVNULL) + return status != 0 + + +def backup_partitions(disk: str): + """Save sfdisk partitions output to a backup file""" + + device_path = '/dev/' + disk + backup_ts = datetime.now().strftime('%Y-%m-%d-%H:%M') + backup_file = '/var/tmp/backup_{}.{}'.format(disk, backup_ts) + cmd = 'sudo /sbin/sfdisk -d {} > {}'.format(device_path, backup_file) + subprocess.check_call([cmd], shell=True) + + +def list_partitions(disk: str): + """List partition numbers of a given disk""" + + parts = set() + part_num_expr = re.compile(disk + '([0-9]+)') + with open('/proc/partitions') as partitions_file: + for line in partitions_file: + fields = line.strip().split() + if len(fields) == 4 and fields[3] != 'name' and part_num_expr.match(fields[3]): + part_idx = part_num_expr.match(fields[3]).group(1) + parts.add(int(part_idx)) + return parts + + +def delete_partition(disk: str, partition_idx: int): + cmd = 'sudo /sbin/parted /dev/{} rm {}'.format(disk, partition_idx) + subprocess.check_call([cmd], shell=True) + + +def format_disk_like(target: str, proto: str): + cmd = 'sudo /sbin/sfdisk -d /dev/{} | sudo /sbin/sfdisk --force /dev/{}'.format(proto, target) + subprocess.check_call([cmd], shell=True) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + group = parser.add_argument_group() + group.add_argument('-t', '--target', type=str, required=True, help='Target device to format') + group.add_argument('-p', '--proto', type=str, required=True, help='Prototype device to use as reference') + args = parser.parse_args() + + if not is_admin(): + print('Must be admin or root to format disk') + sys.exit(1) + + target_disk = args.target + eligible_target_disks = list_disks() + + proto_disk = args.proto + eligible_proto_disks = eligible_target_disks.copy() + eligible_proto_disks.remove(target_disk) + + fmt = { + 'target_disk': target_disk, + 'proto_disk': proto_disk, + } + + if proto_disk == target_disk: + print('The two disk drives must be different.') + sys.exit(1) + + if not os.path.exists('/dev/' + proto_disk): + print('Device /dev/{proto_disk} does not exist'.format_map(fmt)) + sys.exit(1) + + if not os.path.exists('/dev/' + target_disk): + print('Device /dev/{target_disk} does not exist'.format_map(fmt)) + sys.exit(1) + + if target_disk not in eligible_target_disks: + print('Device {target_disk} can not be formatted'.format_map(fmt)) + sys.exit(1) + + if proto_disk not in eligible_proto_disks: + print('Device {proto_disk} can not be used as a prototype for {target_disk}'.format_map(fmt)) + sys.exit(1) + + if is_busy(target_disk): + print("Disk device {target_disk} is busy. Can't format it now".format_map(fmt)) + sys.exit(1) + + print('This will re-format disk {target_disk} so that it has the same disk\n' + 'partion sizes and offsets as {proto_disk}. This will not copy\n' + 'data from {proto_disk} to {target_disk}. But this will erase all\n' + 'data on {target_disk}.\n'.format_map(fmt)) + + if not ask_yes_no("Do you wish to proceed?"): + print('OK. Disk drive {target_disk} will not be re-formated'.format_map(fmt)) + sys.exit(0) + + print('OK. Re-formating disk drive {target_disk}...'.format_map(fmt)) + + print('Making backup copy of partitions...') + backup_partitions(target_disk) + sleep(1) + + print('Deleting old partitions...') + for p in list_partitions(target_disk): + delete_partition(disk=target_disk, partition_idx=p) + + print('Creating new partitions on {target_disk} based on {proto_disk}...'.format_map(fmt)) + format_disk_like(target=target_disk, proto=proto_disk) + print('Done.') diff --git a/src/op_mode/generate_ssh_server_key.py b/src/op_mode/generate_ssh_server_key.py new file mode 100755 index 000000000..f205919b8 --- /dev/null +++ b/src/op_mode/generate_ssh_server_key.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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 subprocess +import sys + +from vyos.util import ask_yes_no + +if not ask_yes_no('Do you really want to remove the existing SSH host keys?'): + sys.exit(0) +else: + subprocess.check_call(['sudo rm -v /etc/ssh/ssh_host_*'], shell=True) + subprocess.check_call(['sudo dpkg-reconfigure openssh-server'], shell=True) + subprocess.check_call(['sudo systemctl restart ssh'], shell=True) diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py index 2f6112fb7..e3644e063 100755 --- a/src/op_mode/powerctrl.py +++ b/src/op_mode/powerctrl.py @@ -22,20 +22,7 @@ import re from datetime import datetime, timedelta, time as type_time, date as type_date from subprocess import check_output, CalledProcessError, STDOUT - -def yn(msg, default=False): - default_msg = "[Y/n]" if default else "[y/N]" - while True: - sys.stdout.write("%s %s " % (msg,default_msg)) - c = input().lower() - if c == '': - return default - elif c in ("y", "ye","yes"): - return True - elif c in ("n", "no"): - return False - else: - sys.stdout.write("Please respond with yes/y or no/n\n") +from vyos.util import ask_yes_no def valid_time(s): @@ -80,7 +67,7 @@ def cancel_shutdown(): def execute_shutdown(time, reboot = True, ask=True): if not ask: action = "reboot" if reboot else "poweroff" - if not yn("Are you sure you want to %s this system?" % action): + if not ask_yes_no("Are you sure you want to %s this system?" % action): sys.exit(0) action = "-r" if reboot else "-P" diff --git a/src/op_mode/show_openvpn.py b/src/op_mode/show_openvpn.py new file mode 100755 index 000000000..23a8156ec --- /dev/null +++ b/src/op_mode/show_openvpn.py @@ -0,0 +1,169 @@ +#!/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 jinja2 +import argparse + +from vyos.config import Config + +outp_tmpl = """ +{% if clients %} +OpenVPN status on {{ intf }} + +Client CN Remote Host Local Host TX bytes RX bytes Connected Since +--------- ----------- ---------- -------- -------- --------------- +{%- for c in clients %} +{{ "%-15s"|format(c.name) }} {{ "%-21s"|format(c.remote) }} {{ "%-21s"|format(local) }} {{ "%-9s"|format(c.tx_bytes) }} {{ "%-9s"|format(c.tx_bytes) }} {{ c.online_since }} +{%- endfor %} +{% endif %} +""" + +def bytes2HR(size): + # we need to operate in integers + size = int(size) + + suff = ['B', 'KB', 'MB', 'GB', 'TB'] + suffIdx = 0 + + while size > 1024: + # incr. suffix index + suffIdx += 1 + # divide + size = size/1024.0 + + output="{0:.1f} {1}".format(size, suff[suffIdx]) + return output + +def get_status(mode, interface): + status_file = '/opt/vyatta/etc/openvpn/status/{}.status'.format(interface) + # this is an empirical value - I assume we have no more then 999999 + # current OpenVPN connections + routing_table_line = 999999 + + data = { + 'mode': mode, + 'intf': interface, + 'local': 'N/A', + 'date': '', + 'clients': [], + } + + with open(status_file, 'r') as f: + lines = f.readlines() + for line_no, line in enumerate(lines): + # remove trailing newline character first + line = line.rstrip('\n') + + # check first line header + if line_no == 0: + if mode == 'server': + if not line == 'OpenVPN CLIENT LIST': + raise NameError('Expected "OpenVPN CLIENT LIST"') + else: + if not line == 'OpenVPN STATISTICS': + raise NameError('Expected "OpenVPN STATISTICS"') + + continue + + # second line informs us when the status file has been last updated + if line_no == 1: + data['date'] = line.lstrip('Updated,').rstrip('\n') + continue + + if mode == 'server': + # followed by line3 giving output information and the actual output data + # + # Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since + # client1,172.18.202.10:55904,2880587,2882653,Fri Aug 23 16:25:48 2019 + # client3,172.18.204.10:41328,2850832,2869729,Fri Aug 23 16:25:43 2019 + # client2,172.18.203.10:48987,2856153,2871022,Fri Aug 23 16:25:45 2019 + if (line_no >= 3) and (line_no < routing_table_line): + # indicator that there are no more clients and we will continue with the + # routing table + if line == 'ROUTING TABLE': + routing_table_line = line_no + continue + + client = { + 'name': line.split(',')[0], + 'remote': line.split(',')[1], + 'rx_bytes': bytes2HR(line.split(',')[2]), + 'tx_bytes': bytes2HR(line.split(',')[3]), + 'online_since': line.split(',')[4] + } + + data['clients'].append(client) + continue + else: + if line_no == 2: + client = { + 'name': 'N/A', + 'remote': 'N/A', + 'rx_bytes': bytes2HR(line.split(',')[1]), + 'tx_bytes': '', + 'online_since': 'N/A' + } + continue + + if line_no == 3: + client['tx_bytes'] = bytes2HR(line.split(',')[1]) + data['clients'].append(client) + break + + return data + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-m', '--mode', help='OpenVPN operation mode (server, client, site-2-site)', required=True) + + args = parser.parse_args() + + # Do nothing if service is not configured + config = Config() + if len(config.list_effective_nodes('interfaces openvpn')) == 0: + print("No OpenVPN interfaces configured") + sys.exit(0) + + # search all OpenVPN interfaces and add those with a matching mode to our + # interfaces list + interfaces = [] + for intf in config.list_effective_nodes('interfaces openvpn'): + # get interface type (server, client, site-to-site) + mode = config.return_effective_value('interfaces openvpn {} mode'.format(intf)) + if args.mode == mode: + interfaces.append(intf) + + for intf in interfaces: + data = get_status(args.mode, intf) + local_host = config.return_effective_value('interfaces openvpn {} local-host'.format(intf)) + local_port = config.return_effective_value('interfaces openvpn {} local-port'.format(intf)) + if local_host and local_port: + data['local'] = local_host + ':' + local_port + + if args.mode in ['client', 'site-to-site']: + for client in data['clients']: + if config.exists_effective('interfaces openvpn {} shared-secret-key-file'.format(intf)): + client['name'] = "None (PSK)" + + remote_host = config.return_effective_values('interfaces openvpn {} remote-host'.format(intf)) + remote_port = config.return_effective_value('interfaces openvpn {} remote-port'.format(intf)) + if len(remote_host) >= 1: + client['remote'] = str(remote_host[0]) + ':' + remote_port + + tmpl = jinja2.Template(outp_tmpl) + print(tmpl.render(data)) + diff --git a/src/op_mode/toggle_help_binding.sh b/src/op_mode/toggle_help_binding.sh new file mode 100755 index 000000000..a8708f3da --- /dev/null +++ b/src/op_mode/toggle_help_binding.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# +# Copyright (C) 2019 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/>. + +# Script for [un-]binding the question mark key for getting help +if [ "$1" == 'disable' ]; then + sed -i "/^bind '\"?\": .* # vyatta key binding$/d" $HOME/.bashrc + echo "bind '\"?\": self-insert' # vyatta key binding" >> $HOME/.bashrc + bind '"?": self-insert' +else + sed -i "/^bind '\"?\": .* # vyatta key binding$/d" $HOME/.bashrc + bind '"?": possible-completions' +fi diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py index 66622c04c..4e93ec6aa 100755 --- a/src/op_mode/wireguard.py +++ b/src/op_mode/wireguard.py @@ -19,91 +19,139 @@ import argparse import os import sys +import shutil 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' + 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: - sl.syslog(sl.LOG_ERR, "modprobe wireguard failed") - 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") - else: - sl.syslog(sl.LOG_NOTICE, "new keypair wireguard key generated in " + dir) - -def genkey(): - """ helper function to check, regenerate the keypair """ - old_umask = os.umask(0o077) - if os.path.exists(pk) and os.path.exists(pub): - try: - 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() - except KeyboardInterrupt: - sys.exit(0) - else: - """ if keypair is bing executed from a running iso """ - if not os.path.exists(dir): - os.umask(old_umask) - subprocess.call(['sudo mkdir -p ' + dir], shell=True) - subprocess.call(['sudo chgrp vyattacfg ' + dir], shell=True) - subprocess.call(['sudo chmod 770 ' + dir], shell=True) - generate_keypair() - os.umask(old_umask) + """ 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: + sl.syslog(sl.LOG_ERR, "modprobe wireguard failed") + raise ConfigError("modprobe wireguard failed") -def showkey(key): - """ helper function to show privkey or pubkey """ - if key == "pub": - if os.path.exists(pub): - print ( open(pub).read().strip() ) + +def generate_keypair(pk, pub): + """ generates a keypair which is stored in /config/auth/wireguard """ + old_umask = os.umask(0o027) + ret = subprocess.call( + ['wg genkey | tee ' + pk + '|wg pubkey > ' + pub], shell=True) + if ret != 0: + raise ConfigError("wireguard key-pair generation failed") else: - print("no public key found") + sl.syslog( + sl.LOG_NOTICE, "new keypair wireguard key generated in " + dir) + os.umask(old_umask) - if key == "pk": - if os.path.exists(pk): - print ( open(pk).read().strip() ) + +def genkey(location): + """ helper function to check, regenerate the keypair """ + pk = "{}/private.key".format(location) + pub = "{}/public.key".format(location) + old_umask = os.umask(0o027) + if os.path.exists(pk) and os.path.exists(pub): + try: + choice = input( + "You already have a wireguard key-pair, do you want to re-generate? [y/n] ") + if choice == 'y' or choice == 'Y': + generate_keypair(pk, pub) + except KeyboardInterrupt: + sys.exit(0) else: - print("no private key found") + """ if keypair is bing executed from a running iso """ + if not os.path.exists(location): + subprocess.call(['sudo mkdir -p ' + location], shell=True) + subprocess.call(['sudo chgrp vyattacfg ' + location], shell=True) + subprocess.call(['sudo chmod 750 ' + location], shell=True) + generate_keypair(pk, pub) + os.umask(old_umask) + + +def showkey(key): + """ helper function to show privkey or pubkey """ + if os.path.exists(key): + print (open(key).read().strip()) + else: + print ("{} not found".format(key)) + def genpsk(): - """ generates a preshared key and shows it on stdout, it's stroed only in the config """ - subprocess.call(['wg genpsk'], shell=True) + """ + generates a preshared key and shows it on stdout, + it's stored only in the cli config + """ + + subprocess.call(['wg genpsk'], shell=True) + +def list_key_dirs(): + """ lists all dirs under /config/auth/wireguard """ + if os.path.exists(dir): + nks = next(os.walk(dir))[1] + for nk in nks: + print (nk) + +def del_key_dir(kname): + """ deletes /config/auth/wireguard/<kname> """ + kdir = "{0}/{1}".format(dir,kname) + if not os.path.isdir(kdir): + print ("named keypair {} not found".format(kname)) + return 1 + shutil.rmtree(kdir) + if __name__ == '__main__': - check_kmod() - - parser = argparse.ArgumentParser(description='wireguard key management') - 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: - if args.genkey: - genkey() - if args.showpub: - showkey("pub") - if args.showpriv: - showkey("pk") - if args.genpsk: - genpsk() - - except ConfigError as e: - print(e) - sys.exit(1) + check_kmod() + parser = argparse.ArgumentParser(description='wireguard key management') + 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') + parser.add_argument( + '--location', action="store", help='key location within {}'.format(dir)) + parser.add_argument( + '--listkdir', action="store_true", help='lists named keydirectories') + parser.add_argument( + '--delkdir', action="store_true", help='removes named keydirectories') + args = parser.parse_args() + + try: + if args.genkey: + if args.location: + genkey("{0}/{1}".format(dir, args.location)) + else: + genkey("{}/default".format(dir)) + if args.showpub: + if args.location: + showkey("{0}/{1}/public.key".format(dir, args.location)) + else: + showkey("{}/default/public.key".format(dir)) + if args.showpriv: + if args.location: + showkey("{0}/{1}/private.key".format(dir, args.location)) + else: + showkey("{}/default/private.key".format(dir)) + if args.genpsk: + genpsk() + if args.listkdir: + list_key_dirs() + if args.delkdir: + if args.location: + del_key_dir(args.location) + else: + del_key_dir("default") + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd new file mode 100755 index 000000000..8f70eb4e9 --- /dev/null +++ b/src/services/vyos-hostsd @@ -0,0 +1,284 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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 os +import sys +import time +import json +import signal +import traceback + +import zmq + +import jinja2 + +debug = True + +DATA_DIR = "/var/lib/vyos/" +STATE_FILE = os.path.join(DATA_DIR, "hostsd.state") + +SOCKET_PATH = "ipc:///run/vyos-hostsd.sock" + +RESOLV_CONF_FILE = '/etc/resolv.conf' +HOSTS_FILE = '/etc/hosts' + +hosts_tmpl_source = """ +### Autogenerated by VyOS ### +### Do not edit, your changes will get overwritten ### + +# Local host +127.0.0.1 localhost +127.0.1.1 {{ host_name }}{% if domain_name %}.{{ domain_name }}{% endif %} + +# The following lines are desirable for IPv6 capable hosts +::1 localhost ip6-localhost ip6-loopback +fe00::0 ip6-localnet +ff00::0 ip6-mcastprefix +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters + +# From DHCP and "system static host-mapping" +{%- if hosts %} +{% for h in hosts -%} +{{hosts[h]['address']}}\t{{h}}\t{% for a in hosts[h]['aliases'] %} {{a}} {% endfor %} +{% endfor %} +{%- endif %} +""" + +hosts_tmpl = jinja2.Template(hosts_tmpl_source) + +resolv_tmpl_source = """ +### Autogenerated by VyOS ### +### Do not edit, your changes will get overwritten ### + +{% for ns in name_servers -%} +nameserver {{ns}} +{% endfor -%} + +{%- if domain_name %} +domain {{ domain_name }} +{%- endif %} + +{%- if search_domains %} +search {{ search_domains | join(" ") }} +{%- endif %} + +""" + +resolv_tmpl = jinja2.Template(resolv_tmpl_source) + +# The state data includes a list of name servers +# and a list of hosts entries. +# +# Name servers have the following structure: +# {"server": {"tag": <str>}} +# +# Hosts entries are similar: +# {"host": {"tag": <str>, "address": <str>, "aliases": <str list>}} +# +# The tag is either "static" or "dhcp-<intf>" +# It's used to distinguish entries created +# by different scripts so that they can be removed +# and re-created without having to track what needs +# to be changed +STATE = { + "name_servers": {}, + "hosts": {}, + "host_name": "vyos", + "domain_name": "", + "search_domains": []} + + +def make_resolv_conf(data): + resolv_conf = resolv_tmpl.render(data) + print("Writing /etc/resolv.conf") + with open(RESOLV_CONF_FILE, 'w') as f: + f.write(resolv_conf) + +def make_hosts_file(state): + print("Writing /etc/hosts") + hosts = hosts_tmpl.render(state) + with open(HOSTS_FILE, 'w') as f: + f.write(hosts) + +def add_hosts(data, entries, tag): + hosts = data['hosts'] + + if not entries: + return + + for e in entries: + host = e['host'] + hosts[host] = {} + hosts[host]['tag'] = tag + hosts[host]['address'] = e['address'] + hosts[host]['aliases'] = e['aliases'] + +def delete_hosts(data, tag): + hosts = data['hosts'] + keys_for_deletion = [] + + # You can't delete items from a dict while iterating over it, + # so we build a list of doomed items first + for h in hosts: + if hosts[h]['tag'] == tag: + keys_for_deletion.append(h) + + for k in keys_for_deletion: + del hosts[k] + +def add_name_servers(data, entries, tag): + name_servers = data['name_servers'] + + if not entries: + return + + for e in entries: + name_servers[e] = {} + name_servers[e]['tag'] = tag + +def delete_name_servers(data, tag): + name_servers = data['name_servers'] + keys_for_deletion = [] + + for ns in name_servers: + if name_servers[ns]['tag'] == tag: + keys_for_deletion.append(ns) + + for k in keys_for_deletion: + del name_servers[k] + +def set_host_name(state, data): + if data['host_name']: + state['host_name'] = data['host_name'] + if data['domain_name']: + state['domain_name'] = data['domain_name'] + if data['search_domains']: + state['search_domains'] = data['search_domains'] + +def get_name_servers(state, tag): + ns = [] + data = state['name_servers'] + for n in data: + if data[n]['tag'] == tag: + ns.append(n) + return ns + +def get_option(msg, key): + if key in msg: + return msg[key] + else: + raise ValueError("Missing required option \"{0}\"".format(key)) + +def handle_message(msg_json): + msg = json.loads(msg_json) + + op = get_option(msg, 'op') + _type = get_option(msg, 'type') + + if op == 'delete': + tag = get_option(msg, 'tag') + + if _type == 'name_servers': + delete_name_servers(STATE, tag) + elif _type == 'hosts': + delete_hosts(STATE, tag) + else: + raise ValueError("Unknown message type {0}".format(_type)) + elif op == 'add': + tag = get_option(msg, 'tag') + entries = get_option(msg, 'data') + if _type == 'name_servers': + add_name_servers(STATE, entries, tag) + elif _type == 'hosts': + add_hosts(STATE, entries, tag) + else: + raise ValueError("Unknown message type {0}".format(_type)) + elif op == 'set': + # Host name/domain name/search domain are set without a tag, + # there can be only one anyway + data = get_option(msg, 'data') + if _type == 'host_name': + set_host_name(STATE, data) + else: + raise ValueError("Unknown message type {0}".format(_type)) + elif op == 'get': + tag = get_option(msg, 'tag') + if _type == 'name_servers': + result = get_name_servers(STATE, tag) + else: + raise ValueError("Unimplemented") + return result + else: + raise ValueError("Unknown operation {0}".format(op)) + + make_resolv_conf(STATE) + make_hosts_file(STATE) + + print("Saving state to {0}".format(STATE_FILE)) + with open(STATE_FILE, 'w') as f: + json.dump(STATE, f) + +def exit_handler(sig, frame): + """ Clean up the state when shutdown correctly """ + print("Cleaning up state") + os.unlink(STATE_FILE) + sys.exit(0) + + +if __name__ == '__main__': + signal.signal(signal.SIGTERM, exit_handler) + + # Create a directory for state checkpoints + os.makedirs(DATA_DIR, exist_ok=True) + if os.path.exists(STATE_FILE): + with open(STATE_FILE, 'r') as f: + try: + data = json.load(f) + STATE = data + except: + print(traceback.format_exc()) + print("Failed to load the state file, using default") + + context = zmq.Context() + socket = context.socket(zmq.REP) + socket.bind(SOCKET_PATH) + + while True: + # Wait for next request from client + message = socket.recv().decode() + print("Received a configuration change request") + if debug: + print("Request data: {0}".format(message)) + + resp = {} + + try: + result = handle_message(message) + resp['data'] = result + except ValueError as e: + resp['error'] = str(e) + except: + print(traceback.format_exc()) + resp['error'] = "Internal error" + + if debug: + print("Sent response: {0}".format(resp)) + + # Send reply back to client + socket.send(json.dumps(resp).encode()) diff --git a/src/system/unpriv-ip b/src/system/unpriv-ip new file mode 100755 index 000000000..1ea0d626a --- /dev/null +++ b/src/system/unpriv-ip @@ -0,0 +1,2 @@ +#!/bin/sh +sudo /sbin/ip $* diff --git a/src/systemd/vyos-hostsd.service b/src/systemd/vyos-hostsd.service new file mode 100644 index 000000000..731e570c9 --- /dev/null +++ b/src/systemd/vyos-hostsd.service @@ -0,0 +1,31 @@ +[Unit] +Description=VyOS DNS configuration keeper + +# Without this option, lots of default dependencies are added, +# among them network.target, which creates a dependency cycle +DefaultDependencies=no + +# Seemingly sensible way to say "as early as the system is ready" +# All vyos-hostsd needs is read/write mounted root +After=systemd-remount-fs.service + +[Service] +ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-hostsd +Type=idle +KillMode=process + +SyslogIdentifier=vyos-hostsd +SyslogFacility=daemon + +Restart=on-failure + +# Does't work in Jessie but leave it here +User=root +Group=vyattacfg + +[Install] + +# Note: After= doesn't actually create a dependency, +# it just sets order for the case when both services are to start, +# and without RequiredBy it *does not* set vyos-hostsd to start. +RequiredBy=cloud-init-local.service vyos-router.service diff --git a/src/utils/vyos-hostsd-client b/src/utils/vyos-hostsd-client new file mode 100755 index 000000000..d3105c9cf --- /dev/null +++ b/src/utils/vyos-hostsd-client @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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 argparse + +import vyos.hostsd_client + +parser = argparse.ArgumentParser() +group = parser.add_mutually_exclusive_group() +group.add_argument('--add-hosts', action="store_true") +group.add_argument('--delete-hosts', action="store_true") +group.add_argument('--add-name-servers', action="store_true") +group.add_argument('--delete-name-servers', action="store_true") +group.add_argument('--set-host-name', action="store_true") + +parser.add_argument('--host', type=str, action="append") +parser.add_argument('--name-server', type=str, action="append") +parser.add_argument('--host-name', type=str) +parser.add_argument('--domain-name', type=str) +parser.add_argument('--search-domain', type=str, action="append") + +parser.add_argument('--tag', type=str) + +args = parser.parse_args() + +try: + client = vyos.hostsd_client.Client() + + if args.add_hosts: + if not args.tag: + raise ValueError("Tag is required for this operation") + data = [] + for h in args.host: + entry = {} + params = h.split(",") + if len(params) < 2: + raise ValueError("Malformed host entry") + entry['host'] = params[0] + entry['address'] = params[1] + entry['aliases'] = params[2:] + data.append(entry) + client.add_hosts(args.tag, data) + elif args.delete_hosts: + if not args.tag: + raise ValueError("Tag is required for this operation") + client.delete_hosts(args.tag) + elif args.add_name_servers: + if not args.tag: + raise ValueError("Tag is required for this operation") + client.add_name_servers(args.tag, args.name_server) + elif args.delete_name_servers: + if not args.tag: + raise ValueError("Tag is required for this operation") + client.delete_name_servers(args.tag) + elif args.set_host_name: + client.set_host_name(args.host_name, args.domain_name, args.search_domain) + else: + raise ValueError("Operation required") + +except ValueError as e: + print("Incorrect options: {0}".format(e)) + sys.exit(1) +except vyos.hostsd_client.VyOSHostsdError as e: + print("Server returned an error: {0}".format(e)) + sys.exit(1) + |