summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/bcast_relay.py167
-rwxr-xr-xsrc/conf_mode/bridge_has_members.py85
-rwxr-xr-xsrc/conf_mode/dhcp_relay.py158
-rwxr-xr-xsrc/conf_mode/dhcp_server.py804
-rwxr-xr-xsrc/conf_mode/dhcpv6_relay.py124
-rwxr-xr-xsrc/conf_mode/dhcpv6_server.py451
-rwxr-xr-xsrc/conf_mode/dynamic_dns.py1
-rwxr-xr-xsrc/conf_mode/mdns_repeater.py92
-rwxr-xr-xsrc/conf_mode/ntp.py8
-rwxr-xr-xsrc/conf_mode/snmp.py276
-rwxr-xr-xsrc/conf_mode/ssh.py67
-rwxr-xr-xsrc/conf_mode/syslog.py14
-rwxr-xr-xsrc/conf_mode/tftp_server.py155
-rwxr-xr-xsrc/conf_mode/vrrp.py5
-rwxr-xr-xsrc/conf_mode/wireguard.py248
15 files changed, 2315 insertions, 340 deletions
diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py
index 95f6215b5..d1257d4a5 100755
--- a/src/conf_mode/bcast_relay.py
+++ b/src/conf_mode/bcast_relay.py
@@ -19,56 +19,104 @@
import sys
import os
import fnmatch
-import subprocess
+import jinja2
from vyos.config import Config
from vyos import ConfigError
config_file = r'/etc/default/udp-broadcast-relay'
+config_tmpl = """
+### Autogenerated by bcast_relay.py ###
+
+# UDP broadcast relay configuration for instance {{ id }}
+{%- if description %}
+# Comment: {{ description }}
+{% endif -%}
+DAEMON_ARGS="{% if address %}-s {{ address }} {% endif %}{{ id }} {{ port }} {{ interfaces | join(' ') }}"
+"""
+
+default_config_data = {
+ 'disabled': False,
+ 'instances': []
+}
+
def get_config():
+ relay = default_config_data
conf = Config()
- conf.set_level("service broadcast-relay id")
- relay_id = conf.list_nodes("")
- relays = []
-
- for id in relay_id:
- interface_list = []
- address = conf.return_value("{0} address".format(id))
- description = conf.return_value("{0} description".format(id))
- port = conf.return_value("{0} port".format(id))
-
- # split the interface name listing and form a list
- if conf.exists("{0} interface".format(id)):
- intfs_names = []
- intfs_names = conf.return_values("{0} interface".format(id))
-
- for name in intfs_names:
- interface_list.append(name)
-
- relay = {
- "id": id,
- "address": address,
- "description": description,
- "interfaces" : interface_list,
- "port": port
+ if not conf.exists('service broadcast-relay'):
+ return None
+ else:
+ conf.set_level('service broadcast-relay')
+
+ # Service can be disabled by user
+ if conf.exists('disable'):
+ relay['disabled'] = True
+ return relay
+
+ # Parse configuration of each individual instance
+ if conf.exists('id'):
+ for id in conf.list_nodes('id'):
+ conf.set_level('service broadcast-relay id {0}'.format(id))
+ config = {
+ 'id': id,
+ 'disabled': False,
+ 'address': '',
+ 'description': '',
+ 'interfaces': [],
+ 'port': ''
}
- relays.append(relay)
- return relays
+ # Check if individual broadcast relay service is disabled
+ if conf.exists('disable'):
+ config['disabled'] = True
+
+ # Source IP of forwarded packets, if empty original senders address is used
+ if conf.exists('address'):
+ config['address'] = conf.return_value('address')
+
+ # A description for each individual broadcast relay service
+ if conf.exists('description'):
+ config['description'] = conf.return_value('description')
+
+ # UDP port to listen on for broadcast frames
+ if conf.exists('port'):
+ config['port'] = conf.return_value('port')
+
+ # Network interfaces to listen on for broadcast frames to be relayed
+ if conf.exists('interface'):
+ config['interfaces'] = conf.return_values('interface')
+
+ relay['instances'].append(config)
-def verify(relays):
- for relay in relays:
- if not relay["port"]:
- raise ConfigError("UDP broadcast relay 'id {0}' requires a port number".format(relay["id"]))
+ return relay
- if len(relay["interfaces"]) < 2:
- raise ConfigError("UDP broadcast relay 'id {0}' requires at least 2 interfaces".format(relay["id"]))
+def verify(relay):
+ if relay is None:
+ return None
+
+ if relay['disabled']:
+ return None
+
+ for r in relay['instances']:
+ # we don't have to check this instance when it's disabled
+ if r['disabled']:
+ continue
+
+ # we certainly require a UDP port to listen to
+ if not r['port']:
+ raise ConfigError('UDP broadcast relay "{0}" requires a port number'.format(r['id']))
+
+ # Relaying data without two interface is kinda senseless ...
+ if len(r['interfaces']) < 2:
+ raise ConfigError('UDP broadcast relay "id {0}" requires at least 2 interfaces'.format(r['id']))
return None
-def generate(relays):
- config_header = '### Autogenerated by bcast_relay.py ###\n'
+
+def generate(relay):
+ if relay is None:
+ return None
config_dir = os.path.dirname(config_file)
config_filename = os.path.basename(config_file)
@@ -82,32 +130,43 @@ def generate(relays):
# sort our list
active_configs.sort()
+ # delete old configuration files
for id in active_configs[:]:
- os.unlink(config_file + id)
-
- for relay in relays:
- file = config_file + str(relay["id"])
- interfaces = ' '.join(str(intf) for intf in relay["interfaces"])
- config_args = 'DAEMON_ARGS="{0} {1}"\n'.format(relay["port"], interfaces)
-
- f = open(file, 'w')
- f.write(config_header)
- if relay["description"]:
- f.write('# ' + relay["description"] + '\n')
- f.write(config_args)
- f.close()
+ if os.path.exists(config_file + id):
+ os.unlink(config_file + id)
+
+ # If the service is disabled, we can bail out here
+ if relay['disabled']:
+ print('Warning: UDP broadcast relay service will be deactivated because it is disabled')
+ return None
+
+ for r in relay['instances']:
+ # Skip writing instance config when it's disabled
+ if r['disabled']:
+ continue
+
+ # configuration filename contains instance id
+ file = config_file + str(r['id'])
+ tmpl = jinja2.Template(config_tmpl)
+ config_text = tmpl.render(r)
+ with open(file, 'w') as f:
+ f.write(config_text)
return None
-def apply(relays):
+def apply(relay):
# first stop all running services
- cmd = "sudo systemctl stop udp-broadcast-relay@{1..99}"
- os.system(cmd)
+ os.system('sudo systemctl stop udp-broadcast-relay@{1..99}')
+
+ if (relay is None) or relay['disabled']:
+ return None
# start only required service instances
- for relay in relays:
- cmd = "sudo systemctl start udp-broadcast-relay@{0}".format(relay["id"])
- os.system(cmd)
+ for r in relay['instances']:
+ # Don't start individual instance when it's disabled
+ if r['disabled']:
+ continue
+ os.system('sudo systemctl start udp-broadcast-relay@{0}'.format(r['id']))
return None
diff --git a/src/conf_mode/bridge_has_members.py b/src/conf_mode/bridge_has_members.py
new file mode 100755
index 000000000..712a9cc46
--- /dev/null
+++ b/src/conf_mode/bridge_has_members.py
@@ -0,0 +1,85 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import sys
+
+import vyos.config
+
+if len(sys.argv) < 2:
+ print("Argument (bridge interface name) is required")
+ sys.exit(1)
+else:
+ bridge = sys.argv[1]
+
+c = vyos.config.Config()
+
+members = []
+
+
+# Check in ethernet and bonding interfaces
+for p in ["interfaces ethernet", "interfaces bonding"]:
+ intfs = c.list_nodes(p)
+ for i in intfs:
+ intf_bridge_path = "{0} {1} bridge-group bridge".format(p, i)
+ if c.exists(intf_bridge_path):
+ intf_bridge = c.return_value(intf_bridge_path)
+ if intf_bridge == bridge:
+ members.append(i)
+ # Walk VLANs
+ for v in c.list_nodes("{0} {1} vif".format(p, i)):
+ vif_bridge_path = "{0} {1} vif {2} bridge-group bridge".format(p, i, v)
+ if c.exists(vif_bridge_path):
+ vif_bridge = c.return_value(vif_bridge_path)
+ if vif_bridge == bridge:
+ members.append("{0}.{1}".format(i, v))
+ # Walk QinQ interfaces
+ for vs in c.list_nodes("{0} {1} vif-s".format(p, i)):
+ vifs_bridge_path = "{0} {1} vif-s {2} bridge-group bridge".format(p, i, vs)
+ if c.exists(vifs_bridge_path):
+ vifs_bridge = c.return_value(vifs_bridge_path)
+ if vifs_bridge == bridge:
+ members.append("{0}.{1}".format(i, vs))
+ for vc in c.list_nodes("{0} {1} vif-s {2} vif-c".format(p, i, vs)):
+ vifc_bridge_path = "{0} {1} vif-s {2} vif-c {3} bridge-group bridge".format(p, i, vs, vc)
+ if c.exists(vifc_bridge_path):
+ vifc_bridge = c.return_value(vifc_bridge_path)
+ if vifc_bridge == bridge:
+ members.append("{0}.{1}.{2}".format(i, vs, vc))
+
+# Check tunnel interfaces
+for t in c.list_nodes("interfaces tunnel"):
+ tunnel_bridge_path = "interfaces tunnel {0} parameters ip bridge-group bridge".format(t)
+ if c.exists(tunnel_bridge_path):
+ intf_bridge = c.return_value(tunnel_bridge_path)
+ if intf_bridge == bridge:
+ members.append(t)
+
+# Check OpenVPN interfaces
+for o in c.list_nodes("interfaces openvpn"):
+ ovpn_bridge_path = "interfaces openvpn {0} bridge-group bridge".format(o)
+ if c.exists(ovpn_bridge_path):
+ intf_bridge = c.return_value(ovpn_bridge_path)
+ if intf_bridge == bridge:
+ members.append(o)
+
+if members:
+ print("Bridge {0} cannot be deleted because some interfaces are configured as its members".format(bridge))
+ print("The following interfaces are members of {0}: {1}".format(bridge, " ".join(members)))
+ sys.exit(1)
+else:
+ sys.exit(0)
diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py
new file mode 100755
index 000000000..61b494b7e
--- /dev/null
+++ b/src/conf_mode/dhcp_relay.py
@@ -0,0 +1,158 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import sys
+import os
+import jinja2
+
+from vyos.config import Config
+from vyos import ConfigError
+
+config_file = r'/etc/default/isc-dhcp-relay'
+
+# Please be careful if you edit the template.
+config_tmpl = """
+### Autogenerated by dhcp_relay.py ###
+
+# Defaults for isc-dhcp-relay initscript
+# sourced by /etc/init.d/isc-dhcp-relay
+
+#
+# This is a POSIX shell fragment
+#
+
+# What servers should the DHCP relay forward requests to?
+SERVERS="{{ server | join(' ') }}"
+
+# On what interfaces should the DHCP relay (dhrelay) serve DHCP requests?
+INTERFACES="{{ interface | join(' ') }}"
+
+# Additional options that are passed to the DHCP relay daemon?
+OPTIONS="-4 {% if port -%} -p {{ port }}{%- endif %} {{ options | join(' ') }}"
+"""
+
+default_config_data = {
+ 'interface': [],
+ 'server': [],
+ 'options': [],
+ 'port': '',
+ 'hop_count': '10',
+ 'relay_agent_packets': 'forward'
+}
+
+def get_config():
+ relay = default_config_data
+ conf = Config()
+ if not conf.exists('service dhcp-relay'):
+ return None
+ else:
+ conf.set_level('service dhcp-relay')
+
+ # Network interfaces to listen on
+ if conf.exists('interface'):
+ relay['interface'] = conf.return_values('interface')
+
+ # Servers equal to the address of the DHCP server(s)
+ if conf.exists('server'):
+ relay['server'] = conf.return_values('server')
+
+ conf.set_level('service dhcp-relay relay-options')
+
+ if conf.exists('hop-count'):
+ count = '-c ' + conf.return_value('hop-count')
+ relay['options'].append(count)
+
+ # Specify the maximum packet size to send to a DHCPv4/BOOTP server.
+ # This might be done to allow sufficient space for addition of relay agent
+ # options while still fitting into the Ethernet MTU size.
+ #
+ # Available in DHCPv4 mode only:
+ if conf.exists('max-size'):
+ size = '-A ' + conf.return_value('max-size')
+ relay['options'].append(size)
+
+ # Listen and transmit on port <xy>. This is mostly useful for debugging
+ # purposes. Default is port 67 for DHCPv4/BOOTP, or port 547 for DHCPv6.
+ if conf.exists('port'):
+ relay['port'] = conf.return_value('port')
+
+ # Control the handling of incoming DHCPv4 packets which already contain
+ # relay agent options. If such a packet does not have giaddr set in its
+ # header, the DHCP standard requires that the packet be discarded. However,
+ # if giaddr is set, the relay agent may handle the situation in four ways:
+ # It may append its own set of relay options to the packet, leaving the
+ # supplied option field intact; it may replace the existing agent option
+ # field; it may forward the packet unchanged; or, it may discard it.
+ #
+ # Available in DHCPv4 mode only:
+ if conf.exists('relay-agents-packets'):
+ pkt = '-m ' + conf.return_value('relay-agents-packets')
+ relay['options'].append(pkt)
+
+ return relay
+
+def verify(relay):
+ # bail out early - looks like removal from running config
+ if relay is None:
+ return None
+
+ if len(relay['interface']) < 2:
+ # We can only issue a warning otherwise old configurations might break
+ print('WARNING: At least two interfaces are required for DHCP relay\n' \
+ 'to work\n')
+
+ if 'lo' in relay['interface']:
+ raise ConfigError('DHCP relay does not support the loopback interface.')
+
+ if len(relay['server']) == 0:
+ raise ConfigError('No DHCP relay server(s) configured.\n' \
+ 'At least one DHCP relay server required.')
+
+ return None
+
+def generate(relay):
+ # bail out early - looks like removal from running config
+ if relay is None:
+ return None
+
+ tmpl = jinja2.Template(config_tmpl)
+ config_text = tmpl.render(relay)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
+
+ return None
+
+def apply(relay):
+ if relay is not None:
+ os.system('sudo systemctl restart isc-dhcp-relay.service')
+ else:
+ # DHCP relay support is removed in the commit
+ os.system('sudo systemctl stop isc-dhcp-relay.service')
+ os.unlink(config_file)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
new file mode 100755
index 000000000..2a2b1fe6c
--- /dev/null
+++ b/src/conf_mode/dhcp_server.py
@@ -0,0 +1,804 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import sys
+import os
+import ipaddress
+import jinja2
+import socket
+import struct
+
+import vyos.validate
+
+from vyos.config import Config
+from vyos import ConfigError
+
+config_file = r'/etc/dhcp/dhcpd.conf'
+lease_file = r'/config/dhcpd.leases'
+daemon_config_file = r'/etc/default/isc-dhcp-server'
+
+# Please be careful if you edit the template.
+config_tmpl = """
+### Autogenerated by dhcp_server.py ###
+
+# For options please consult the following website:
+# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html
+#
+# log-facility local7;
+
+{% if hostfile_update %}
+on commit {
+ set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name);
+ set ClientIp = binary-to-ascii(10, 8, ".", leased-address);
+ set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6));
+ set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!");
+ execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "commit", ClientName, ClientIp, ClientMac, ClientDomain);
+}
+
+on release {
+ set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name);
+ set ClientIp = binary-to-ascii(10, 8, ".",leased-address);
+ set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6));
+ set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!");
+ execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain);
+}
+
+on expiry {
+ set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name);
+ set ClientIp = binary-to-ascii(10, 8, ".",leased-address);
+ set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6));
+ set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!");
+ execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain);
+}
+{% endif %}
+ddns-update-style {% if ddns_enable -%} interim {%- else -%} none {%- endif %};
+{% if static_route -%}
+option rfc3442-static-route code 121 = array of integer 8;
+option windows-static-route code 249 = array of integer 8;
+{%- endif %}
+{% if static_route -%}
+option wpad-url code 252 = text;
+{% endif %}
+
+{%- if global_parameters %}
+# The following {{ global_parameters | length }} line(s) were added as global-parameters in the CLI and have not been validated
+{%- for param in global_parameters %}
+{{ param }}
+{%- endfor -%}
+{%- endif %}
+
+# Failover configuration
+{% for network in shared_network %}
+{%- if not network.disabled -%}
+{%- for subnet in network.subnet %}
+{%- if subnet.failover_name -%}
+failover peer "{{ subnet.failover_name }}" {
+{%- if subnet.failover_status == 'primary' %}
+ primary;
+ mclt 1800;
+ split 128;
+{%- elif subnet.failover_status == 'secondary' %}
+ secondary;
+{%- endif %}
+ address {{ subnet.failover_local_addr }};
+ port 520;
+ peer address {{ subnet.failover_peer_addr }};
+ peer port 520;
+ max-response-delay 30;
+ max-unacked-updates 10;
+ load balance max seconds 3;
+}
+{% endif -%}
+{% endfor -%}
+{% endif -%}
+{% endfor %}
+
+# Shared network configration(s)
+{% for network in shared_network %}
+{%- if not network.disabled -%}
+shared-network {{ network.name }} {
+ {{ "authoritative;" if network.authoritative }}
+ {%- if network.network_parameters %}
+ # The following {{ network.network_parameters | length }} line(s) were added as shared-network-parameters in the CLI and have not been validated
+ {%- for param in network.network_parameters %}
+ {{ param }}
+ {%- endfor %}
+ {%- endif %}
+ {%- for subnet in network.subnet %}
+ subnet {{ subnet.address }} netmask {{ subnet.netmask }} {
+ {%- if subnet.dns_server %}
+ option domain-name-servers {{ subnet.dns_server | join(', ') }};
+ {%- endif %}
+ {%- if subnet.domain_search %}
+ option domain-search {{ subnet.domain_search | join(', ') }};
+ {%- endif %}
+ {%- if subnet.ntp_server %}
+ option ntp-servers {{ subnet.ntp_server | join(', ') }};
+ {%- endif %}
+ {%- if subnet.pop_server %}
+ option pop-server {{ subnet.pop_server | join(', ') }};
+ {%- endif %}
+ {%- if subnet.smtp_server %}
+ option smtp-server {{ subnet.smtp_server | join(', ') }};
+ {%- endif %}
+ {%- if subnet.time_server %}
+ option time-servers {{ subnet.time_server | join(', ') }};
+ {%- endif %}
+ {%- if subnet.wins_server %}
+ option netbios-name-servers {{ subnet.wins_server | join(', ') }};
+ {%- endif %}
+ {%- if subnet.static_route %}
+ option rfc3442-static-route {{ subnet.static_route }};
+ option windows-static-route {{ subnet.static_route }};
+ {%- endif %}
+ {%- if subnet.ip_forwarding %}
+ option ip-forwarding true;
+ {%- endif -%}
+ {%- if subnet.default_router %}
+ option routers {{ subnet.default_router }};
+ {%- endif -%}
+ {%- if subnet.server_identifier %}
+ option dhcp-server-identifier {{ subnet.server_identifier }};
+ {%- endif -%}
+ {%- if subnet.domain_name %}
+ option domain-name "{{ subnet.domain_name }}";
+ {%- endif -%}
+ {%- if subnet.tftp_server %}
+ option tftp-server-name "{{ subnet.tftp_server }}";
+ {%- endif -%}
+ {%- if subnet.bootfile_name %}
+ option bootfile-name "{{ subnet.bootfile_name }}";
+ filename "{{ subnet.bootfile_name }}";
+ {%- endif -%}
+ {%- if subnet.bootfile_server %}
+ next-server {{ subnet.bootfile_server }};
+ {%- endif -%}
+ {%- if subnet.time_offset %}
+ option time-offset {{ subnet.time_offset }};
+ {%- endif -%}
+ {%- if subnet.wpad_url %}
+ option wpad-url "{{ subnet.wpad_url }}";
+ {%- endif -%}
+ {%- if subnet.client_prefix_length %}
+ option subnet-mask {{ subnet.client_prefix_length }};
+ {%- endif -%}
+ {% if subnet.lease %}
+ default-lease-time {{ subnet.lease }};
+ max-lease-time {{ subnet.lease }};
+ {%- endif -%}
+ {%- for host in subnet.static_mapping %}
+ {% if not host.disabled -%}
+ host {{ network.name }}_{{ host.name }} {
+ fixed-address {{ host.ip_address }};
+ hardware ethernet {{ host.mac_address }};
+ {%- if host.static_parameters %}
+ # The following {{ host.static_parameters | length }} line(s) were added as static-mapping-parameters in the CLI and have not been validated
+ {%- for param in host.static_parameters %}
+ {{ param }}
+ {%- endfor -%}
+ {%- endif %}
+ }
+ {%- endif %}
+ {%- endfor %}
+ {%- if subnet.failover_name %}
+ pool {
+ failover peer "{{ subnet.failover_name }}";
+ deny dynamic bootp clients;
+ {%- for range in subnet.range %}
+ range {{ range.start }} {{ range.stop }};
+ {%- endfor %}
+ }
+ {%- else %}
+ {%- for range in subnet.range %}
+ range {{ range.start }} {{ range.stop }};
+ {%- endfor %}
+ {%- endif %}
+ }
+ {%- endfor %}
+ on commit { set shared-networkname = "{{ network.name }}"; }
+}
+{%- endif %}
+{% endfor %}
+"""
+
+daemon_tmpl = """
+### Autogenerated by dhcp_server.py ###
+
+# sourced by /etc/init.d/isc-dhcp-server
+
+DHCPD_CONF=/etc/dhcp/dhcpd.conf
+DHCPD_PID=/var/run/dhcpd.pid
+OPTIONS="-4 -lf {{ lease_file }}"
+INTERFACES=""
+"""
+
+default_config_data = {
+ 'lease_file': lease_file,
+ 'disabled': False,
+ 'ddns_enable': False,
+ 'global_parameters': [],
+ 'hostfile_update': False,
+ 'static_route': False,
+ 'wpad': False,
+ 'shared_network': [],
+}
+
+def get_config():
+ dhcp = default_config_data
+ conf = Config()
+ if not conf.exists('service dhcp-server'):
+ return None
+ else:
+ conf.set_level('service dhcp-server')
+
+ # check for global disable of DHCP service
+ if conf.exists('disable'):
+ dhcp['disabled'] = True
+
+ # check for global dynamic DNS upste
+ if conf.exists('dynamic-dns-update'):
+ dhcp['ddns_enable'] = True
+
+ # HACKS AND TRICKS
+ #
+ # check for global 'raw' ISC DHCP parameters configured by users
+ # actually this is a bad idea in general to pass raw parameters from any user
+ if conf.exists('global-parameters'):
+ dhcp['global_parameters'] = conf.return_values('global-parameters')
+
+ # check for global DHCP server updating /etc/host per lease
+ if conf.exists('hostfile-update'):
+ dhcp['hostfile_update'] = True
+
+ # check for multiple, shared networks served with DHCP addresses
+ if conf.exists('shared-network-name'):
+ for network in conf.list_nodes('shared-network-name'):
+ conf.set_level('service dhcp-server shared-network-name {0}'.format(network))
+ config = {
+ 'name': network,
+ 'authoritative': False,
+ 'description': '',
+ 'disabled': False,
+ 'network_parameters': [],
+ 'subnet': []
+ }
+ # check if DHCP server should be authoritative on this network
+ if conf.exists('authoritative'):
+ config['authoritative'] = True
+
+ # A description for this given network
+ if conf.exists('description'):
+ config['description'] = conf.return_value('description')
+
+ # If disabled, the shared-network configuration becomes inactive in
+ # the running DHCP server instance
+ if conf.exists('disable'):
+ config['disabled'] = True
+
+ # HACKS AND TRICKS
+ #
+ # check for 'raw' ISC DHCP parameters configured by users
+ # actually this is a bad idea in general to pass raw parameters
+ # from any user
+ #
+ # deprecate this and issue a warning like we do for DNS forwarding?
+ if conf.exists('shared-network-parameters'):
+ config['network_parameters'] = conf.return_values('shared-network-parameters')
+
+ # check for multiple subnet configurations in a shared network
+ # config segment
+ if conf.exists('subnet'):
+ for net in conf.list_nodes('subnet'):
+ conf.set_level('service dhcp-server shared-network-name {0} subnet {1}'.format(network, net))
+ subnet = {
+ 'network': net,
+ 'address': str(ipaddress.ip_network(net).network_address),
+ 'netmask': str(ipaddress.ip_network(net).netmask),
+ 'bootfile_name': '',
+ 'bootfile_server': '',
+ 'client_prefix_length': '',
+ 'default_router': '',
+ 'dns_server': [],
+ 'domain_name': '',
+ 'domain_search': [],
+ 'exclude': [],
+ 'failover_local_addr': '',
+ 'failover_name': '',
+ 'failover_peer_addr': '',
+ 'failover_status': '',
+ 'ip_forwarding': False,
+ 'lease': '86400',
+ 'ntp_server': [],
+ 'pop_server': [],
+ 'server_identifier': '',
+ 'smtp_server': [],
+ 'range': [],
+ 'static_mapping': [],
+ 'static_subnet': '',
+ 'static_router': '',
+ 'static_route': '',
+ 'subnet_parameters': [],
+ 'tftp_server': '',
+ 'time_offset': '',
+ 'time_server': [],
+ 'wins_server': [],
+ 'wpad_url': ''
+ }
+
+ # Used to identify a bootstrap file
+ if conf.exists('bootfile-name'):
+ subnet['bootfile_name'] = conf.return_value('bootfile-name')
+
+ # Specify host address of the server from which the initial boot file
+ # (specified above) is to be loaded. Should be a numeric IP address or
+ # domain name.
+ if conf.exists('bootfile-server'):
+ subnet['bootfile_server'] = conf.return_value('bootfile-server')
+
+ # The subnet mask option specifies the client's subnet mask as per RFC 950. If no subnet
+ # mask option is provided anywhere in scope, as a last resort dhcpd will use the subnet
+ # mask from the subnet declaration for the network on which an address is being assigned.
+ if conf.exists('client-prefix-length'):
+ # snippet borrowed from https://stackoverflow.com/questions/33750233/convert-cidr-to-subnet-mask-in-python
+ host_bits = 32 - int(conf.return_value('client-prefix-length'))
+ subnet['client_prefix_length'] = socket.inet_ntoa(struct.pack('!I', (1 << 32) - (1 << host_bits)))
+
+ # Default router IP address on the client's subnet
+ if conf.exists('default-router'):
+ subnet['default_router'] = conf.return_value('default-router')
+
+ # Specifies a list of Domain Name System (STD 13, RFC 1035) name servers available to
+ # the client. Servers should be listed in order of preference.
+ if conf.exists('dns-server'):
+ subnet['dns_server'] = conf.return_values('dns-server')
+
+ # Option specifies the domain name that client should use when resolving hostnames
+ # via the Domain Name System.
+ if conf.exists('domain-name'):
+ subnet['domain_name'] = conf.return_value('domain-name')
+
+ # The domain-search option specifies a 'search list' of Domain Names to be used
+ # by the client to locate not-fully-qualified domain names.
+ if conf.exists('domain-search'):
+ for domain in conf.return_values('domain-search'):
+ subnet['domain_search'].append('"' + domain + '"')
+
+ # IP address (local) for failover peer to connect
+ if conf.exists('failover local-address'):
+ subnet['failover_local_addr'] = conf.return_value('failover local-address')
+
+ # DHCP failover peer name
+ if conf.exists('failover name'):
+ subnet['failover_name'] = conf.return_value('failover name')
+
+ # IP address (remote) of failover peer
+ if conf.exists('failover peer-address'):
+ subnet['failover_peer_addr'] = conf.return_value('failover peer-address')
+
+ # DHCP failover peer status (primary|secondary)
+ if conf.exists('failover status'):
+ subnet['failover_status'] = conf.return_value('failover status')
+
+ # Option specifies whether the client should configure its IP layer for packet
+ # forwarding
+ if conf.exists('ip-forwarding'):
+ subnet['ip_forwarding'] = True
+
+ # Time should be the length in seconds that will be assigned to a lease if the
+ # client requesting the lease does not ask for a specific expiration time
+ if conf.exists('lease'):
+ subnet['lease'] = conf.return_value('lease')
+
+ # Specifies a list of IP addresses indicating NTP (RFC 5905) servers available
+ # to the client.
+ if conf.exists('ntp-server'):
+ subnet['ntp_server'] = conf.return_values('ntp-server')
+
+ # POP3 server option specifies a list of POP3 servers available to the client.
+ # Servers should be listed in order of preference.
+ if conf.exists('pop-server'):
+ subnet['pop_server'] = conf.return_values('pop-server')
+
+ # DHCP servers include this option in the DHCPOFFER in order to allow the client
+ # to distinguish between lease offers. DHCP clients use the contents of the
+ # 'server identifier' field as the destination address for any DHCP messages
+ # unicast to the DHCP server
+ if conf.exists('server-identifier'):
+ subnet['server_identifier'] = conf.return_value('server-identifier')
+
+ # SMTP server option specifies a list of SMTP servers available to the client.
+ # Servers should be listed in order of preference.
+ if conf.exists('smtp-server'):
+ subnet['smtp_server'] = conf.return_values('smtp-server')
+
+ # For any subnet on which addresses will be assigned dynamically, there must be at
+ # least one range statement. The range statement gives the lowest and highest IP
+ # addresses in a range. All IP addresses in the range should be in the subnet in
+ # which the range statement is declared.
+ if conf.exists('range'):
+ for range in conf.list_nodes('range'):
+ range = {
+ 'start': conf.return_value('range {0} start'.format(range)),
+ 'stop': conf.return_value('range {0} stop'.format(range))
+ }
+ subnet['range'].append(range)
+
+ # IP address that needs to be excluded from DHCP lease range
+ if conf.exists('exclude'):
+ # We have no need to store the exclude addresses. Exclude addresses
+ # are recalculated into several ranges
+ exclude = []
+ subnet['exclude'] = conf.return_values('exclude')
+ for addr in subnet['exclude']:
+ exclude.append(ipaddress.ip_address(addr))
+
+ # sort excluded IP addresses ascending
+ exclude = sorted(exclude)
+
+ # calculate multipe ranges based on the excluded IP addresses
+ output = []
+ for range in subnet['range']:
+ range_start = range['start']
+ range_stop = range['stop']
+
+ for i in exclude:
+ # Excluded IP address must be in out specified range
+ if (i >= ipaddress.ip_address(range_start)) and (i <= ipaddress.ip_address(range_stop)):
+ # Build up new IP address range ending one IP address before
+ # our exclude address
+ range = {
+ 'start': str(range_start),
+ 'stop': str(i - 1)
+ }
+ # Our next IP address range will start one address after
+ # our exclude address
+ range_start = i + 1
+ output.append(range)
+
+ # Take care of last IP address range spanning from the last exclude
+ # address (+1) to the end of the initial configured range
+ if i is exclude[-1]:
+ last = {
+ 'start': str(i + 1),
+ 'stop': str(range_stop)
+ }
+ output.append(last)
+ else:
+ # IP address not inside search range, take range is it is
+ output.append(range)
+
+ # We successfully build up a new list containing several IP address
+ # ranges, replace IP address range in our dictionary
+ subnet['range'] = output
+
+ # Static DHCP leases
+ if conf.exists('static-mapping'):
+ for mapping in conf.list_nodes('static-mapping'):
+ conf.set_level('service dhcp-server shared-network-name {0} subnet {1} static-mapping {2}'.format(network, net, mapping))
+ mapping = {
+ 'name': mapping,
+ 'disabled': False,
+ 'ip_address': '',
+ 'mac_address': '',
+ 'static_parameters': []
+ }
+
+ # This static lease is disabled
+ if conf.exists('disable'):
+ mapping['disabled'] = True
+
+ # IP address used for this DHCP client
+ if conf.exists('ip-address'):
+ mapping['ip_address'] = conf.return_value('ip-address')
+
+ # MAC address of requesting DHCP client
+ if conf.exists('mac-address'):
+ mapping['mac_address'] = conf.return_value('mac-address')
+
+ # HACKS AND TRICKS
+ #
+ # check for 'raw' ISC DHCP parameters configured by users
+ # actually this is a bad idea in general to pass raw parameters
+ # from any user
+ #
+ # deprecate this and issue a warning like we do for DNS forwarding?
+ if conf.exists('static-mapping-parameters'):
+ mapping['static_parameters'] = conf.return_values('static-mapping-parameters')
+
+ # append static-mapping configuration to subnet list
+ subnet['static_mapping'].append(mapping)
+
+ # Reset config level to matching hirachy
+ conf.set_level('service dhcp-server shared-network-name {0} subnet {1}'.format(network, net))
+
+ # This option specifies a list of static routes that the client should install in its routing
+ # cache. If multiple routes to the same destination are specified, they are listed in descending
+ # order of priority.
+ if conf.exists('static-route destination-subnet'):
+ subnet['static_subnet'] = conf.return_value('static-route destination-subnet')
+ # Required for global config section
+ dhcp['static_route'] = True
+
+ if conf.exists('static-route router'):
+ subnet['static_router'] = conf.return_value('static-route router')
+
+ if subnet['static_router'] and subnet['static_subnet']:
+ # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server
+ # Option format is:
+ # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3>
+ # where bytes with the value 0 are omitted.
+ net = ipaddress.ip_network(subnet['static_subnet'])
+ # add netmask
+ string = str(net.prefixlen) + ','
+ # add network bytes
+ bytes = str(net.network_address).split('.')
+ for b in bytes:
+ if b != '0':
+ string += b + ','
+
+ # add router bytes
+ bytes = subnet['static_router'].split('.')
+ for b in bytes:
+ if b != '0':
+ string += b
+ if b is not bytes[-1]:
+ string += ','
+
+ subnet['static_route'] = string
+
+ # HACKS AND TRICKS
+ #
+ # check for 'raw' ISC DHCP parameters configured by users
+ # actually this is a bad idea in general to pass raw parameters
+ # from any user
+ #
+ # deprecate this and issue a warning like we do for DNS forwarding?
+ if conf.exists('subnet-parameters'):
+ config['subnet_parameters'] = conf.return_values('subnet-parameters')
+
+ # This option is used to identify a TFTP server and, if supported by the client, should have
+ # the same effect as the server-name declaration. BOOTP clients are unlikely to support this
+ # option. Some DHCP clients will support it, and others actually require it.
+ if conf.exists('tftp-server-name'):
+ subnet['tftp_server'] = conf.return_value('tftp-server-name')
+
+ # The time-offset option specifies the offset of the client’s subnet in seconds from
+ # Coordinated Universal Time (UTC).
+ if conf.exists('time-offset'):
+ subnet['time_offset'] = conf.return_value('time-offset')
+
+ # The time-server option specifies a list of RFC 868 time servers available to the client.
+ # Servers should be listed in order of preference.
+ if conf.exists('time-server'):
+ subnet['time_server'] = conf.return_values('time-server')
+
+ # The NetBIOS name server (NBNS) option specifies a list of RFC 1001/1002 NBNS name servers
+ # listed in order of preference. NetBIOS Name Service is currently more commonly referred to
+ # as WINS. WINS servers can be specified using the netbios-name-servers option.
+ if conf.exists('wins-server'):
+ subnet['wins_server'] = conf.return_values('wins-server')
+
+ # URL for Web Proxy Autodiscovery Protocol
+ if conf.exists('wpad-url'):
+ subnet['wpad_url'] = conf.return_value('wpad-url')
+ # Required for global config section
+ dhcp['wpad'] = True
+
+ # append subnet configuration to shared network subnet list
+ config['subnet'].append(subnet)
+
+ # append shared network configuration to config dictionary
+ dhcp['shared_network'].append(config)
+
+ return dhcp
+
+def verify(dhcp):
+ if (dhcp is None) or (dhcp['disabled'] is True):
+ return None
+
+ # If DHCP is enabled we need one share-network
+ if len(dhcp['shared_network']) == 0:
+ raise ConfigError('No DHCP shared networks configured.\n' \
+ 'At least one DHCP shared network must be configured.')
+
+ # Inspect shared-network/subnet
+ failover_names = []
+ listen_ok = False
+ subnets = []
+
+ # A shared-network requires a subnet definition
+ for network in dhcp['shared_network']:
+ if len(network['subnet']) == 0:
+ raise ConfigError('No DHCP lease subnets configured for {0}. At least one\n' \
+ 'lease subnet must be configured for each shared network.'.format(network['name']))
+
+ for subnet in network['subnet']:
+ # Subnet static route declaration requires destination and router
+ if subnet['static_subnet'] or subnet['static_router']:
+ if not (subnet['static_subnet'] and subnet['static_router']):
+ raise ConfigError('Please specify missing DHCP static-route parameter(s):\n' \
+ 'destination-subnet | router')
+
+ # Failover requires all 4 parameters set
+ if subnet['failover_local_addr'] or subnet['failover_peer_addr'] or subnet['failover_name'] or subnet['failover_status']:
+ if not (subnet['failover_local_addr'] and subnet['failover_peer_addr'] and subnet['failover_name'] and subnet['failover_status']):
+ raise ConfigError('Please specify missing DHCP failover parameter(s):\n' \
+ 'local-address | peer-address | name | status')
+
+ # Failover names must be uniquie
+ if subnet['failover_name'] in failover_names:
+ raise ConfigError('Failover names must be unique:\n' \
+ '{0} has already been configured!'.format(subnet['failover_name']))
+ else:
+ failover_names.append(subnet['failover_name'])
+
+ # Failover requires start/stop ranges for pool
+ if (len(subnet['range']) == 0):
+ raise ConfigError('At least one start-stop range must be configured for {0}\n' \
+ 'to set up DHCP failover!'.format(subnet['network']))
+
+ # Check if DHCP address range is inside configured subnet declaration
+ range_start = []
+ range_stop = []
+ for range in subnet['range']:
+ start = range['start']
+ stop = range['stop']
+ # DHCP stop IP required after start IP
+ if start and not stop:
+ raise ConfigError('DHCP range stop address for start {0} is not defined!'.format(start))
+
+ # Start address must be inside network
+ if not ipaddress.ip_address(start) in ipaddress.ip_network(subnet['network']):
+ raise ConfigError('DHCP range start address {0} is not in subnet {1}\n' \
+ 'specified for shared network {2}!'.format(start, subnet['network'], network['name']))
+
+ # Stop address must be inside network
+ if not ipaddress.ip_address(stop) in ipaddress.ip_network(subnet['network']):
+ raise ConfigError('DHCP range stop address {0} is not in subnet {1}\n' \
+ 'specified for shared network {2}!'.format(stop, subnet['network'], network['name']))
+
+ # Stop address must be greater or equal to start address
+ if not ipaddress.ip_address(stop) >= ipaddress.ip_address(start):
+ raise ConfigError('DHCP range stop address {0} must be greater or equal\n' \
+ 'to the range start address {1}!'.format(stop, start))
+
+ # Range start address must be unique
+ if start in range_start:
+ raise ConfigError('Conflicting DHCP lease range:\n' \
+ 'Pool start address {0} defined multipe times!'.format(start))
+ else:
+ range_start.append(start)
+
+ # Range stop address must be unique
+ if stop in range_stop:
+ raise ConfigError('Conflicting DHCP lease range:\n' \
+ 'Pool stop address {0} defined multipe times!'.format(stop))
+ else:
+ range_stop.append(stop)
+
+ # Exclude addresses must be in bound
+ for exclude in subnet['exclude']:
+ if not ipaddress.ip_address(exclude) in ipaddress.ip_network(subnet['network']):
+ raise ConfigError('Exclude IP address {0} is outside of the DHCP lease network {1}\n' \
+ 'under shared network {2}!'.format(exclude, subnet['network'], network['name']))
+
+ # At least one DHCP address range or static-mapping required
+ active_mapping = False
+ if (len(subnet['range']) == 0):
+ for mapping in subnet['static_mapping']:
+ # we need at least one active mapping
+ if (not active_mapping) and (not mapping['disabled']):
+ active_mapping = True
+ else:
+ active_mapping = True
+
+ if not active_mapping:
+ raise ConfigError('No DHCP address range or active static-mapping set\n' \
+ 'for subnet {0}!'.format(subnet['network']))
+
+ # Static IP address mappings require both an IP address and MAC address
+ for mapping in subnet['static_mapping']:
+ # Static IP address must be configured
+ if not mapping['ip_address']:
+ raise ConfigError('DHCP static lease IP address not specified for static mapping\n' \
+ '{0} under shared network name {1}!'.format(mapping['name'], network['name']))
+
+ # Static IP address must be in bound
+ if not ipaddress.ip_address(mapping['ip_address']) in ipaddress.ip_network(subnet['network']):
+ raise ConfigError('DHCP static lease IP address {0} for static mapping {1}\n' \
+ 'in shared network {2} is outside DHCP lease subnet {3}!' \
+ .format(mapping['ip_address'], mapping['name'], network['name'], subnet['network']))
+
+ # Static mapping requires MAC address
+ if not mapping['mac_address']:
+ raise ConfigError('DHCP static lease MAC address not specified for static mapping\n' \
+ '{0} under shared network name {1}!'.format(mapping['name'], network['name']))
+
+ # There must be one subnet connected to a listen interface.
+ # This only counts if the network itself is not disabled!
+ if not network['disabled']:
+ if vyos.validate.is_subnet_connected(subnet['network'], primary=True):
+ listen_ok = True
+
+ # Subnets must be non overlapping
+ if subnet['network'] in subnets:
+ raise ConfigError('DHCP subnets must be unique! Subnet {0} defined multiple times!'.format(subnet))
+ else:
+ subnets.append(subnet['network'])
+
+ # Check for overlapping subnets
+ net = ipaddress.ip_network(subnet['network'])
+ for n in subnets:
+ net2 = ipaddress.ip_network(n)
+ if (net != net2):
+ if net.overlaps(net2):
+ raise ConfigError('DHCP conflicting subnet ranges: {0} overlaps {1}'.format(net, net2))
+
+ if not listen_ok:
+ raise ConfigError('None of the DHCP lease subnets are inside any configured subnet on\n' \
+ 'broadcast interfaces. At least one lease subnet must be set such that\n' \
+ 'DHCP server listens on a one broadcast interface!')
+
+ return None
+
+def generate(dhcp):
+ if dhcp is None:
+ return None
+
+ if dhcp['disabled'] is True:
+ print('Warning: DHCP server will be deactivated because it is disabled')
+ return None
+
+ tmpl = jinja2.Template(config_tmpl)
+ config_text = tmpl.render(dhcp)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
+
+ tmpl = jinja2.Template(daemon_tmpl)
+ config_text = tmpl.render(dhcp)
+ with open(daemon_config_file, 'w') as f:
+ f.write(config_text)
+
+ return None
+
+def apply(dhcp):
+ if (dhcp is None) or dhcp['disabled']:
+ # DHCP server is removed in the commit
+ os.system('sudo systemctl stop isc-dhcp-server.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
+ if os.path.exists(daemon_config_file):
+ os.unlink(daemon_config_file)
+ else:
+ # If our file holding DHCP leases does yet not exist - create it
+ if not os.path.exists(lease_file):
+ os.mknod(lease_file)
+
+ os.system('sudo systemctl restart isc-dhcp-server.service')
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py
new file mode 100755
index 000000000..959bf0496
--- /dev/null
+++ b/src/conf_mode/dhcpv6_relay.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import sys
+import os
+import jinja2
+
+from vyos.config import Config
+from vyos import ConfigError
+
+config_file = r'/etc/default/isc-dhcpv6-relay'
+
+# Please be careful if you edit the template.
+config_tmpl = """
+### Autogenerated by dhcpv6_relay.py ###
+
+# Defaults for isc-dhcpv6-relay initscript sourced by /etc/init.d/isc-dhcpv6-relay
+
+OPTIONS="-6 -l {{ listen_addr | join('-l ') }} {% if port -%} -p {{ port }}{%- endif %} {{ options | join(' ') }} -u {{ upstream_addr | join('-u ') }}"
+"""
+
+default_config_data = {
+ 'listen_addr': [],
+ 'upstream_addr': [],
+ 'port': '',
+ 'options': [],
+}
+
+def get_config():
+ relay = default_config_data
+ conf = Config()
+ if not conf.exists('service dhcpv6-relay'):
+ return None
+ else:
+ conf.set_level('service dhcpv6-relay')
+
+ # Network interfaces/address to listen on for DHCPv6 query(s)
+ if conf.exists('listen-interface'):
+ interfaces = conf.list_nodes('listen-interface')
+ for intf in interfaces:
+ addr = conf.return_value('listen-interface {0} address'.format(intf))
+ listen = addr + '%' + intf
+ relay['listen_addr'].append(listen)
+
+ # Upstream interface/address for remote DHCPv6 server
+ if conf.exists('upstream-interface'):
+ interfaces = conf.list_nodes('upstream-interface')
+ for intf in interfaces:
+ addr = conf.return_value('upstream-interface {0} address'.format(intf))
+ server = addr + '%' + intf
+ relay['upstream_addr'].append(server)
+
+ # Listen and transmit on port <xy>. This is mostly useful for debugging
+ # purposes. Default is port 67 for DHCPv4/BOOTP, or port 547 for DHCPv6.
+ if conf.exists('listen-port'):
+ relay['port'] = conf.return_value('listen-port')
+
+ # Maximum hop count. When forwarding packets, dhcrelay discards packets
+ # which have reached a hop count of COUNT. Default is 10. Maximum is 255.
+ if conf.exists('max-hop-count'):
+ count = '-c ' + conf.return_value('max-hop-count')
+ relay['options'].append(count)
+
+ if conf.exists('use-interface-id-option'):
+ relay['options'].append('-I')
+
+ return relay
+
+def verify(relay):
+ # bail out early - looks like removal from running config
+ if relay is None:
+ return None
+
+ if len(relay['listen_addr']) == 0 or len(relay['upstream_addr']) == 0:
+ raise ConfigError('Must set at least one listen and upstream interface.')
+
+ return None
+
+def generate(relay):
+ # bail out early - looks like removal from running config
+ if relay is None:
+ return None
+
+ tmpl = jinja2.Template(config_tmpl)
+ config_text = tmpl.render(relay)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
+
+ return None
+
+def apply(relay):
+ if relay is not None:
+ os.system('sudo systemctl restart isc-dhcpv6-relay.service')
+ else:
+ # DHCPv6 relay support is removed in the commit
+ os.system('sudo systemctl stop isc-dhcpv6-relay.service')
+ os.unlink(config_file)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py
new file mode 100755
index 000000000..bb3e6e90d
--- /dev/null
+++ b/src/conf_mode/dhcpv6_server.py
@@ -0,0 +1,451 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import sys
+import os
+import ipaddress
+
+import jinja2
+
+import vyos.validate
+
+from vyos.config import Config
+from vyos import ConfigError
+
+config_file = r'/etc/dhcp/dhcpd6.conf'
+lease_file = r'/config/dhcpd6.leases'
+daemon_config_file = r'/etc/default/isc-dhcpv6-server'
+
+# Please be careful if you edit the template.
+config_tmpl = """
+### Autogenerated by dhcpv6_server.py ###
+
+# For options please consult the following website:
+# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html
+
+log-facility local7;
+{%- if preference %}
+option dhcp6.preference {{ preference }};
+{%- endif %}
+
+# Shared network configration(s)
+{% for network in shared_network %}
+{%- if not network.disabled -%}
+shared-network {{ network.name }} {
+ {%- for subnet in network.subnet %}
+ subnet6 {{ subnet.network }} {
+ {%- for range in subnet.range6_prefix %}
+ range6 {{ range.prefix }}{{ " temporary" if range.temporary }};
+ {%- endfor %}
+ {%- for range in subnet.range6 %}
+ range6 {{ range.start }} {{ range.stop }};
+ {%- endfor %}
+ {%- if subnet.domain_search %}
+ option dhcp6.domain-search {{ subnet.domain_search | join(', ') }};
+ {%- endif %}
+ {%- if subnet.lease_def %}
+ default-lease-time {{ subnet.lease_def }};
+ {%- endif %}
+ {%- if subnet.lease_max %}
+ max-lease-time {{ subnet.lease_max }};
+ {%- endif %}
+ {%- if subnet.lease_min %}
+ min-lease-time {{ subnet.lease_min }};
+ {%- endif %}
+ {%- if subnet.dns_server %}
+ option dhcp6.name-servers {{ subnet.dns_server | join(', ') }};
+ {%- endif %}
+ {%- if subnet.nis_domain %}
+ option dhcp6.nis-domain-name "{{ subnet.nis_domain }}";
+ {%- endif %}
+ {%- if subnet.nis_server %}
+ option dhcp6.nis-servers {{ subnet.nis_server | join(', ') }};
+ {%- endif %}
+ {%- if subnet.nisp_domain %}
+ option dhcp6.nisp-domain-name "{{ subnet.nisp_domain }}";
+ {%- endif %}
+ {%- if subnet.nisp_server %}
+ option dhcp6.nisp-servers {{ subnet.nisp_server | join(', ') }};
+ {%- endif %}
+ {%- if subnet.sip_address %}
+ option dhcp6.sip-servers-addresses {{ subnet.sip_address | join(', ') }};
+ {%- endif %}
+ {%- if subnet.sip_hostname %}
+ option dhcp6.sip-servers-names {{ subnet.sip_hostname | join(', ') }};
+ {%- endif %}
+ {%- if subnet.sntp_server %}
+ option dhcp6.sntp-servers {{ subnet.sntp_server | join(', ') }};
+ {%- endif %}
+ {%- for host in subnet.static_mapping %}
+ {% if not host.disabled -%}
+ host {{ network.name }}_{{ host.name }} {
+ host-identifier option dhcp6.client-id "{{ host.client_identifier }}";
+ fixed-address6 {{ host.ipv6_address }};
+ }
+ {%- endif %}
+ {%- endfor %}
+ }
+ {%- endfor %}
+}
+{%- endif %}
+{% endfor %}
+
+"""
+
+daemon_tmpl = """
+### Autogenerated by dhcp_server.py ###
+
+# sourced by /etc/init.d/isc-dhcpv6-server
+
+DHCPD_CONF=/etc/dhcp/dhcpd6.conf
+DHCPD_PID=/var/run/dhcpd6.pid
+OPTIONS="-6 -lf {{ lease_file }}"
+INTERFACES=""
+"""
+
+default_config_data = {
+ 'lease_file': lease_file,
+ 'preference': '',
+ 'disabled': False,
+ 'shared_network': []
+}
+
+def get_config():
+ dhcpv6 = default_config_data
+ conf = Config()
+ if not conf.exists('service dhcpv6-server'):
+ return None
+ else:
+ conf.set_level('service dhcpv6-server')
+
+ # Check for global disable of DHCPv6 service
+ if conf.exists('disable'):
+ dhcpv6['disabled'] = True
+ return dhcpv6
+
+ # Preference of this DHCPv6 server compared with others
+ if conf.exists('preference'):
+ dhcpv6['preference'] = conf.return_value('preference')
+
+ # check for multiple, shared networks served with DHCPv6 addresses
+ if conf.exists('shared-network-name'):
+ for network in conf.list_nodes('shared-network-name'):
+ conf.set_level('service dhcpv6-server shared-network-name {0}'.format(network))
+ config = {
+ 'name': network,
+ 'disabled': False,
+ 'subnet': []
+ }
+
+ # If disabled, the shared-network configuration becomes inactive
+ if conf.exists('disable'):
+ config['disabled'] = True
+
+ # check for multiple subnet configurations in a shared network
+ if conf.exists('subnet'):
+ for net in conf.list_nodes('subnet'):
+ conf.set_level('service dhcpv6-server shared-network-name {0} subnet {1}'.format(network, net))
+ subnet = {
+ 'network': net,
+ 'range6_prefix': [],
+ 'range6': [],
+ 'default_router': '',
+ 'dns_server': [],
+ 'domain_name': '',
+ 'domain_search': [],
+ 'lease_def': '',
+ 'lease_min': '',
+ 'lease_max': '',
+ 'nis_domain': '',
+ 'nis_server': [],
+ 'nisp_domain': '',
+ 'nisp_server': [],
+ 'sip_address': [],
+ 'sip_hostname': [],
+ 'sntp_server': [],
+ 'static_mapping': []
+ }
+
+ # For any subnet on which addresses will be assigned dynamically, there must be at
+ # least one address range statement. The range statement gives the lowest and highest
+ # IP addresses in a range. All IP addresses in the range should be in the subnet in
+ # which the range statement is declared.
+ if conf.exists('address-range prefix'):
+ for prefix in conf.list_nodes('address-range prefix'):
+ range = {
+ 'prefix': prefix,
+ 'temporary': False
+ }
+
+ # Address range will be used for temporary addresses
+ if conf.exists('address-range prefix {0} temporary'.format(range['prefix'])):
+ range['temporary'] = True
+
+ # Append to subnet temporary range6 list
+ subnet['range6_prefix'].append(range)
+
+ if conf.exists('address-range start'):
+ for range in conf.list_nodes('address-range start'):
+ range = {
+ 'start': range,
+ 'stop': conf.return_value('address-range start {0} stop'.format(range))
+ }
+
+ # Append to subnet range6 list
+ subnet['range6'].append(range)
+
+ # The domain-search option specifies a 'search list' of Domain Names to be used
+ # by the client to locate not-fully-qualified domain names.
+ if conf.exists('domain-search'):
+ for domain in conf.return_values('domain-search'):
+ subnet['domain_search'].append('"' + domain + '"')
+
+ # IPv6 address valid lifetime
+ # (at the end the address is no longer usable by the client)
+ # (set to 30 days, the usual IPv6 default)
+ if conf.exists('lease-time default'):
+ subnet['lease_def'] = conf.return_value('lease-time default')
+
+ # Time should be the maximum length in seconds that will be assigned to a lease.
+ # The only exception to this is that Dynamic BOOTP lease lengths, which are not
+ # specified by the client, are not limited by this maximum.
+ if conf.exists('lease-time maximum'):
+ subnet['lease_max'] = conf.return_value('lease-time maximum')
+
+ # Time should be the minimum length in seconds that will be assigned to a lease
+ if conf.exists('lease-time minimum'):
+ subnet['lease_min'] = conf.return_value('lease-time minimum')
+
+ # Specifies a list of Domain Name System name servers available to the client.
+ # Servers should be listed in order of preference.
+ if conf.exists('name-server'):
+ subnet['dns_server'] = conf.return_values('name-server')
+
+ # Ancient NIS (Network Information Service) domain name
+ if conf.exists('nis-domain'):
+ subnet['nis_domain'] = conf.return_value('nis-domain')
+
+ # Ancient NIS (Network Information Service) servers
+ if conf.exists('nis-server'):
+ subnet['nis_server'] = conf.return_values('nis-server')
+
+ # Ancient NIS+ (Network Information Service) domain name
+ if conf.exists('nisplus-domain'):
+ subnet['nisp_domain'] = conf.return_value('nisplus-domain')
+
+ # Ancient NIS+ (Network Information Service) servers
+ if conf.exists('nisplus-server'):
+ subnet['nisp_server'] = conf.return_values('nisplus-server')
+
+ # Prefix Delegation (RFC 3633)
+ if conf.exists('prefix-delegation'):
+ print('TODO: This option is actually not implemented right now!')
+
+ # Local SIP server that is to be used for all outbound SIP requests - IPv6 address
+ if conf.exists('sip-server-address'):
+ subnet['sip_address'] = conf.return_values('sip-server-address')
+
+ # Local SIP server that is to be used for all outbound SIP requests - hostname
+ if conf.exists('sip-server-name'):
+ for hostname in conf.return_values('sip-server-name'):
+ subnet['sip_hostname'].append('"' + hostname + '"')
+
+ # List of local SNTP servers available for the client to synchronize their clocks
+ if conf.exists('sntp-server'):
+ subnet['sntp_server'] = conf.return_values('sntp-server')
+
+ #
+ # Static DHCP v6 leases
+ #
+ if conf.exists('static-mapping'):
+ for mapping in conf.list_nodes('static-mapping'):
+ conf.set_level('service dhcpv6-server shared-network-name {0} subnet {1} static-mapping {2}'.format(network, net, mapping))
+ mapping = {
+ 'name': mapping,
+ 'disabled': False,
+ 'ipv6_address': '',
+ 'client_identifier': '',
+ }
+
+ # This static lease is disabled
+ if conf.exists('disable'):
+ mapping['disabled'] = True
+
+ # IPv6 address used for this DHCP client
+ if conf.exists('ipv6-address'):
+ mapping['ipv6_address'] = conf.return_value('ipv6-address')
+
+ # This option specifies the client’s DUID identifier. DUIDs are similar but different from DHCPv4 client identifiers
+ if conf.exists('identifier'):
+ mapping['client_identifier'] = conf.return_value('identifier')
+
+ # append static mapping configuration tu subnet list
+ subnet['static_mapping'].append(mapping)
+
+ # append subnet configuration to shared network subnet list
+ config['subnet'].append(subnet)
+
+
+ # append shared network configuration to config dictionary
+ dhcpv6['shared_network'].append(config)
+
+ return dhcpv6
+
+def verify(dhcpv6):
+ if dhcpv6 is None:
+ return None
+
+ if dhcpv6['disabled']:
+ return None
+
+ # If DHCP is enabled we need one share-network
+ if len(dhcpv6['shared_network']) == 0:
+ raise ConfigError('No DHCPv6 shared networks configured.\n' \
+ 'At least one DHCPv6 shared network must be configured.')
+
+ # Inspect shared-network/subnet
+ subnets = []
+ listen_ok = False
+
+ for network in dhcpv6['shared_network']:
+ # A shared-network requires a subnet definition
+ if len(network['subnet']) == 0:
+ raise ConfigError('No DHCPv6 lease subnets configured for {0}. At least one\n' \
+ 'lease subnet must be configured for each shared network.'.format(network['name']))
+
+ range6_start = []
+ range6_stop = []
+ for subnet in network['subnet']:
+ # Ususal range declaration with a start and stop address
+ for range6 in subnet['range6']:
+ # shorten names
+ start = range6['start']
+ stop = range6['stop']
+
+ # DHCPv6 stop address is required
+ if start and not stop:
+ raise ConfigError('DHCPv6 range stop address for start {0} is not defined!'.format(start))
+
+ # Start address must be inside network
+ if not ipaddress.ip_address(start) in ipaddress.ip_network(subnet['network']):
+ raise ConfigError('DHCPv6 range start address {0} is not in subnet {1}\n' \
+ 'specified for shared network {2}!'.format(start, subnet['network'], network['name']))
+
+ # Stop address must be inside network
+ if not ipaddress.ip_address(stop) in ipaddress.ip_network(subnet['network']):
+ raise ConfigError('DHCPv6 range stop address {0} is not in subnet {1}\n' \
+ 'specified for shared network {2}!'.format(stop, subnet['network'], network['name']))
+
+ # Stop address must be greater or equal to start address
+ if not ipaddress.ip_address(stop) >= ipaddress.ip_address(start):
+ raise ConfigError('DHCPv6 range stop address {0} must be greater or equal\n' \
+ 'to the range start address {1}!'.format(stop, start))
+
+ # DHCPv6 range start address must be unique - two ranges can't
+ # start with the same address - makes no sense
+ if start in range6_start:
+ raise ConfigError('Conflicting DHCPv6 lease range:\n' \
+ 'Pool start address {0} defined multipe times!'.format(start))
+ else:
+ range6_start.append(start)
+
+ # DHCPv6 range stop address must be unique - two ranges can't
+ # end with the same address - makes no sense
+ if stop in range6_stop:
+ raise ConfigError('Conflicting DHCPv6 lease range:\n' \
+ 'Pool stop address {0} defined multipe times!'.format(stop))
+ else:
+ range6_stop.append(stop)
+
+ # We also have prefixes that require checking
+ for prefix in subnet['range6_prefix']:
+ # If configured prefix does not match our subnet, we have to check that it's inside
+ if ipaddress.ip_network(prefix['prefix']) != ipaddress.ip_network(subnet['network']):
+ # Configured prefixes must be inside our network
+ if not ipaddress.ip_network(prefix['prefix']) in ipaddress.ip_network(subnet['network']):
+ raise ConfigError('DHCPv6 prefix {0} is not in subnet {1}\n' \
+ 'specified for shared network {2}!'.format(prefix['prefix'], subnet['network'], network['name']))
+
+ # DHCPv6 requires at least one configured address range or one static mapping
+ if not network['disabled']:
+ if vyos.validate.is_subnet_connected(subnet['network']):
+ listen_ok = True
+
+ # DHCPv6 subnet must not overlap. ISC DHCP also complains about overlapping
+ # subnets: "Warning: subnet 2001:db8::/32 overlaps subnet 2001:db8:1::/32"
+ net = ipaddress.ip_network(subnet['network'])
+ for n in subnets:
+ net2 = ipaddress.ip_network(n)
+ if (net != net2):
+ if net.overlaps(net2):
+ raise ConfigError('DHCPv6 conflicting subnet ranges: {0} overlaps {1}'.format(net, net2))
+
+ if not listen_ok:
+ raise ConfigError('None of the DHCPv6 subnets are connected to a subnet6 on\n' \
+ 'this machine. At least one subnet6 must be connected such that\n' \
+ 'DHCPv6 listens on an interface!')
+
+
+ return None
+
+def generate(dhcpv6):
+ if dhcpv6 is None:
+ return None
+
+ if dhcpv6['disabled']:
+ print('Warning: DHCPv6 server will be deactivated because it is disabled')
+ return None
+
+ tmpl = jinja2.Template(config_tmpl)
+ config_text = tmpl.render(dhcpv6)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
+
+ tmpl = jinja2.Template(daemon_tmpl)
+ config_text = tmpl.render(dhcpv6)
+ with open(daemon_config_file, 'w') as f:
+ f.write(config_text)
+
+ return None
+
+def apply(dhcpv6):
+ if (dhcpv6 is None) or dhcpv6['disabled']:
+ # DHCP server is removed in the commit
+ os.system('sudo systemctl stop isc-dhcpv6-server.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
+ if os.path.exists(daemon_config_file):
+ os.unlink(daemon_config_file)
+ else:
+ # If our file holding DHCPv6 leases does yet not exist - create it
+ if not os.path.exists(lease_file):
+ os.mknod(lease_file)
+
+ os.system('sudo systemctl restart isc-dhcpv6-server.service')
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py
index 90d4ff567..60efcaae2 100755
--- a/src/conf_mode/dynamic_dns.py
+++ b/src/conf_mode/dynamic_dns.py
@@ -63,6 +63,7 @@ protocol={{ srv.protocol }}
max-interval=28d
login={{ srv.login }}
password='{{ srv.password }}'
+server={{ srv.server }}
{{ host }}
{% endfor %}
{% endfor %}
diff --git a/src/conf_mode/mdns_repeater.py b/src/conf_mode/mdns_repeater.py
index 474a6a5cf..cef735c0d 100755
--- a/src/conf_mode/mdns_repeater.py
+++ b/src/conf_mode/mdns_repeater.py
@@ -18,7 +18,7 @@
import sys
import os
-
+import jinja2
import netifaces
from vyos.config import Config
@@ -26,60 +26,78 @@ from vyos import ConfigError
config_file = r'/etc/default/mdns-repeater'
-def get_config():
- interface_list = []
+config_tmpl = """
+### Autogenerated by mdns_repeater.py ###
+DAEMON_ARGS="{{ interfaces | join(' ') }}"
+"""
+
+default_config_data = {
+ 'disabled': False,
+ 'interfaces': []
+}
+def get_config():
+ mdns = default_config_data
conf = Config()
- conf.set_level('service mdns repeater')
- if not conf.exists(''):
- return interface_list
+ if not conf.exists('service mdns repeater'):
+ return None
+ else:
+ conf.set_level('service mdns repeater')
- if conf.exists('interface'):
- intfs_names = []
- intfs_names = conf.return_values('interface')
+ # Service can be disabled by user
+ if conf.exists('disable'):
+ mdns['disabled'] = True
+ return mdns
- for name in intfs_names:
- interface_list.append(name)
+ # Interface to repeat mDNS advertisements
+ if conf.exists('interface'):
+ mdns['interfaces'] = conf.return_values('interface')
- return interface_list
+ return mdns
def verify(mdns):
- # '0' interfaces are possible, think of service deletion. Only '1' is not supported!
- if len(mdns) == 1:
- raise ConfigError('At least 2 interfaces must be specified but %d given!' % len(mdns))
-
- # For mdns-repeater to work it is essential that the interfaces
- # have an IP address assigned
- for intf in mdns:
- try:
- netifaces.ifaddresses(intf)[netifaces.AF_INET]
- except KeyError as e:
- raise ConfigError('No IP address configured for interface "%s"!' % intf)
+ if mdns is None:
+ return None
+
+ if mdns['disabled']:
+ return None
+
+ # We need at least two interfaces to repeat mDNS advertisments
+ if len(mdns['interfaces']) < 2:
+ raise ConfigError('mDNS repeater requires at least 2 configured interfaces!')
+
+ # For mdns-repeater to work it is essential that the interfaces has
+ # an IPv4 address assigned
+ for interface in mdns['interfaces']:
+ if netifaces.AF_INET in netifaces.ifaddresses(interface).keys():
+ if len(netifaces.ifaddresses(interface)[netifaces.AF_INET]) < 1:
+ raise ConfigError('mDNS repeater requires an IPv6 address configured on interface %s!'.format(interface))
return None
def generate(mdns):
- config_header = '### Autogenerated by mdns_repeater.py ###\n'
- if len(mdns) > 0:
- config_args = 'DAEMON_ARGS="' + ' '.join(str(e) for e in mdns) + '"\n'
- else:
- config_args = 'DAEMON_ARGS=""\n'
+ if mdns is None:
+ return None
+
+ if mdns['disabled']:
+ print('Warning: mDNS repeater will be deactivated because it is disabled')
+ return None
- # write new configuration file
- f = open(config_file, 'w')
- f.write(config_header)
- f.write(config_args)
- f.close()
+ tmpl = jinja2.Template(config_tmpl)
+ config_text = tmpl.render(mdns)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
return None
def apply(mdns):
- if len(mdns) == 0:
- cmd = "sudo systemctl stop mdns-repeater"
+ if (mdns is None) or mdns['disabled']:
+ os.system('sudo systemctl stop mdns-repeater')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
else:
- cmd = "sudo systemctl restart mdns-repeater"
+ os.system('sudo systemctl restart mdns-repeater')
- os.system(cmd)
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py
index 2a6088575..0abb2746a 100755
--- a/src/conf_mode/ntp.py
+++ b/src/conf_mode/ntp.py
@@ -36,7 +36,7 @@ config_tmpl = """
#
driftfile /var/lib/ntp/ntp.drift
# By default, only allow ntpd to query time sources, ignore any incoming requests
-restrict default ignore
+restrict default noquery nopeer notrap nomodify
# Local users have unrestricted access, allowing reconfiguration via ntpdc
restrict 127.0.0.1
restrict -6 ::1
@@ -154,10 +154,10 @@ def generate(ntp):
def apply(ntp):
if ntp is not None:
- os.system('sudo /usr/sbin/invoke-rc.d ntp force-reload')
+ os.system('sudo systemctl restart ntp.service')
else:
- # NTP suuport is removed in the commit
- os.system('sudo /usr/sbin/invoke-rc.d ntp stop')
+ # NTP support is removed in the commit
+ os.system('sudo systemctl stop ntp.service')
os.unlink(config_file)
return None
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py
index 3b47ffc98..7d695bb4c 100755
--- a/src/conf_mode/snmp.py
+++ b/src/conf_mode/snmp.py
@@ -21,15 +21,14 @@ import os
import shutil
import stat
import pwd
-import time
import jinja2
-import ipaddress
import random
import binascii
import re
import vyos.version
+import vyos.validate
from vyos.config import Config
from vyos import ConfigError
@@ -38,6 +37,7 @@ config_file_client = r'/etc/snmp/snmp.conf'
config_file_daemon = r'/etc/snmp/snmpd.conf'
config_file_access = r'/usr/share/snmp/snmpd.conf'
config_file_user = r'/var/lib/snmp/snmpd.conf'
+config_file_init = r'/etc/default/snmpd'
# SNMP OIDs used to mark auth/priv type
OIDs = {
@@ -47,7 +47,7 @@ OIDs = {
'des' : '.1.3.6.1.6.3.10.1.2.2',
'none': '.1.3.6.1.6.3.10.1.2.1'
}
-# SNMPS template - be careful if you edit the template.
+# SNMP template (/etc/snmp/snmp.conf) - be careful if you edit the template.
client_config_tmpl = """
### Autogenerated by snmp.py ###
{% if trap_source -%}
@@ -56,39 +56,38 @@ clientaddr {{ trap_source }}
"""
-# SNMPS template - be careful if you edit the template.
+# SNMP template (/usr/share/snmp/snmpd.conf) - be careful if you edit the template.
access_config_tmpl = """
### Autogenerated by snmp.py ###
-{% if v3_users %}
-{% for u in v3_users %}
+{%- for u in v3_users %}
{{ u.mode }}user {{ u.name }}
-{% endfor %}
-{% endif -%}
+{%- endfor %}
+
rwuser {{ vyos_user }}
"""
-# SNMPS template - be careful if you edit the template.
+# SNMP template (/var/lib/snmp/snmpd.conf) - be careful if you edit the template.
user_config_tmpl = """
### Autogenerated by snmp.py ###
# user
-{% if v3_users %}
-{% for u in v3_users %}
-{% if u.authOID == 'none' %}
+{%- for u in v3_users %}
+{%- if u.authOID == 'none' %}
createUser {{ u.name }}
-{% elif u.authPassword %}
+{%- elif u.authPassword %}
createUser {{ u.name }} {{ u.authProtocol | upper }} "{{ u.authPassword }}" {{ u.privProtocol | upper }} {{ u.privPassword }}
-{% else %}
+{%- else %}
usmUser 1 3 {{ u.engineID }} "{{ u.name }}" "{{ u.name }}" NULL {{ u.authOID }} {{ u.authMasterKey }} {{ u.privOID }} {{ u.privMasterKey }} 0x
-{% endif %}
-{% endfor %}
-{% endif %}
+{%- endif %}
+{%- endfor %}
createUser {{ vyos_user }} MD5 "{{ vyos_user_pass }}" DES
+{%- if v3_engineid %}
oldEngineID {{ v3_engineid }}
+{%- endif %}
"""
-# SNMPS template - be careful if you edit the template.
+# SNMP template (/etc/snmp/snmpd.conf) - be careful if you edit the template.
daemon_config_tmpl = """
### Autogenerated by snmp.py ###
@@ -96,15 +95,10 @@ daemon_config_tmpl = """
sysObjectID 1.3.6.1.4.1.44641
sysServices 14
master agentx
-agentXPerms 0755 0755
+agentXPerms 0777 0777
pass .1.3.6.1.2.1.31.1.1.1.18 /opt/vyatta/sbin/if-mib-alias
smuxpeer .1.3.6.1.2.1.83
smuxpeer .1.3.6.1.2.1.157
-smuxpeer .1.3.6.1.4.1.3317.1.2.2
-smuxpeer .1.3.6.1.4.1.3317.1.2.3
-smuxpeer .1.3.6.1.4.1.3317.1.2.5
-smuxpeer .1.3.6.1.4.1.3317.1.2.8
-smuxpeer .1.3.6.1.4.1.3317.1.2.9
smuxsocket localhost
# linkUp/Down configure the Event MIB tables to monitor
@@ -122,110 +116,109 @@ monitor -r 10 -e linkDownTrap "Generate linkDown" ifOperStatus == 2
########################
# configurable section #
########################
-
{% if v3_tsm_key %}
[snmp] localCert {{ v3_tsm_key }}
-{% endif %}
+{%- endif %}
# Default system description is VyOS version
sysDescr VyOS {{ version }}
-{% if description -%}
+{% if description %}
# Description
SysDescr {{ description }}
-{% endif %}
+{%- endif %}
# Listen
agentaddress unix:/run/snmpd.socket{% if listen_on %}{% for li in listen_on %},{{ li }}{% endfor %}{% else %},udp:161,udp6:161{% endif %}{% if v3_tsm_key %},tlstcp:{{ v3_tsm_port }},dtlsudp::{{ v3_tsm_port }}{% endif %}
# SNMP communities
-{% if communities -%}
-{% for c in communities %}
-{% if c.network -%}
-{% for network in c.network %}
+{%- for c in communities %}
+{%- if c.network_v4 %}
+{%- for network in c.network_v4 %}
{{ c.authorization }}community {{ c.name }} {{ network }}
-{{ c.authorization }}community6 {{ c.name }} {{ network }}
-{% endfor %}
-{% else %}
+{%- endfor %}
+{%- else %}
{{ c.authorization }}community {{ c.name }}
+{%- endif %}
+{%- if c.network_v6 %}
+{%- for network in c.network_v6 %}
+{{ c.authorization }}community6 {{ c.name }} {{ network }}
+{%- endfor %}
+{%- else %}
{{ c.authorization }}community6 {{ c.name }}
-{% endif %}
-{% endfor %}
-{% endif %}
+{%- endif %}
+{%- endfor %}
-{% if contact -%}
+{% if contact %}
# system contact information
SysContact {{ contact }}
-{% endif %}
+{%- endif %}
-{% if location -%}
+{% if location %}
# system location information
SysLocation {{ location }}
-{% endif %}
+{%- endif %}
{% if smux_peers -%}
# additional smux peers
-{% for sp in smux_peers %}
+{%- for sp in smux_peers %}
smuxpeer {{ sp }}
-{% endfor %}
-{% endif %}
+{%- endfor %}
+{%- endif %}
{% if trap_targets -%}
# if there is a problem - tell someone!
-{% for t in trap_targets %}
+{%- for t in trap_targets %}
trap2sink {{ t.target }}{% if t.port -%}:{{ t.port }}{% endif %} {{ t.community }}
-{% endfor %}
-{% endif %}
+{%- endfor %}
+{%- endif %}
+{%- if v3_enabled %}
#
# SNMPv3 stuff goes here
#
-{% if v3_enabled %}
-
# views
-{% if v3_views -%}
-{% for v in v3_views %}
-{% for oid in v.oids %}
+{%- for v in v3_views %}
+{%- for oid in v.oids %}
view {{ v.name }} included .{{ oid.oid }}
-{% endfor %}
-{% endfor %}
-{% endif %}
+{%- endfor %}
+{%- endfor %}
# access
# context sec.model sec.level match read write notif
-{% if v3_groups -%}
-{% for g in v3_groups %}
-{% if g.mode == 'ro' %}
-access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} none none
-access {{ g.name }} "" tsm {{ g.seclevel }} exact {{ g.view }} none none
-{% elif g.mode == 'rw' %}
-access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} {{ g.view }} none
-access {{ g.name }} "" tsm {{ g.seclevel }} exact {{ g.view }} {{ g.view }} none
-{% endif %}
-{% endfor -%}
-{% endif %}
+{%- for g in v3_groups %}
+access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} {% if g.mode == 'ro' %}none{% else %}{{ g.view }}{% endif %} none
+access {{ g.name }} "" tsm {{ g.seclevel }} exact {{ g.view }} {% if g.mode == 'ro' %}none{% else %}{{ g.view }}{% endif %} none
+{%- endfor %}
# trap-target
-{% if v3_traps -%}
-{% for t in v3_traps %}
+{%- for t in v3_traps %}
trapsess -v 3 {{ '-Ci' if t.type == 'inform' }} -e {{ t.engineID }} -u {{ t.secName }} -l {{ t.secLevel }} -a {{ t.authProtocol }} {% if t.authPassword %}-A {{ t.authPassword }}{% elif t.authMasterKey %}-3m {{ t.authMasterKey }}{% endif %} -x {{ t.privProtocol }} {% if t.privPassword %}-X {{ t.privPassword }}{% elif t.privMasterKey %}-3M {{ t.privMasterKey }}{% endif %} {{ t.ipProto }}:{{ t.ipAddr }}:{{ t.ipPort }}
-{% endfor -%}
-{% endif %}
+{%- endfor %}
# group
-{% if v3_users -%}
-{% for u in v3_users %}
+{%- for u in v3_users %}
group {{ u.group }} usm {{ u.name }}
group {{ u.group }} tsm {{ u.name }}
{% endfor %}
-{% endif %}
+{%- endif %}
+"""
-{% endif %}
+# SNMP template (/etc/default/snmpd) - be careful if you edit the template.
+init_config_tmpl = """
+### Autogenerated by snmp.py ###
+# This file controls the activity of snmpd
+
+# snmpd control (yes means start daemon).
+SNMPDRUN=yes
+# snmpd options (use syslog, close stdin/out/err).
+SNMPDOPTS='-LSed -u snmp -g snmp -p /run/snmpd.pid'
"""
default_config_data = {
'listen_on': [],
+ 'listen_address': [],
'communities': [],
'smux_peers': [],
'location' : '',
@@ -271,14 +264,28 @@ def get_config():
community = {
'name': name,
'authorization': 'ro',
- 'network': []
+ 'network_v4': [],
+ 'network_v6': []
}
if conf.exists('community {0} authorization'.format(name)):
community['authorization'] = conf.return_value('community {0} authorization'.format(name))
+ # Subnet of SNMP client(s) allowed to contact system
if conf.exists('community {0} network'.format(name)):
- community['network'] = conf.return_values('community {0} network'.format(name))
+ for addr in conf.return_values('community {0} network'.format(name)):
+ if vyos.validate.is_ipv4(addr):
+ community['network_v4'].append(addr)
+ else:
+ community['network_v6'].append(addr)
+
+ # IP address of SNMP client allowed to contact system
+ if conf.exists('community {0} client'.format(name)):
+ for addr in conf.return_values('community {0} client'.format(name)):
+ if vyos.validate.is_ipv4(addr):
+ community['network_v4'].append(addr)
+ else:
+ community['network_v6'].append(addr)
snmp['communities'].append(community)
@@ -290,21 +297,20 @@ def get_config():
if conf.exists('listen-address'):
for addr in conf.list_nodes('listen-address'):
- listen = ''
port = '161'
if conf.exists('listen-address {0} port'.format(addr)):
port = conf.return_value('listen-address {0} port'.format(addr))
- if ipaddress.ip_address(addr).version == 4:
- # udp:127.0.0.1:161
- listen = 'udp:' + addr + ':' + port
- elif ipaddress.ip_address(addr).version == 6:
- # udp6:[::1]:161
- listen = 'udp6:' + '[' + addr + ']' + ':' + port
- else:
- raise ConfigError('Invalid IP address version')
+ snmp['listen_address'].append((addr, port))
- snmp['listen_on'].append(listen)
+ # Always listen on localhost if an explicit address has been configured
+ # This is a safety measure to not end up with invalid listen addresses
+ # that are not configured on this system. See https://phabricator.vyos.net/T850
+ if not '127.0.0.1' in conf.list_nodes('listen-address'):
+ snmp['listen_address'].append(('127.0.0.1', '161'))
+
+ if not '::1' in conf.list_nodes('listen-address'):
+ snmp['listen_address'].append(('::1', '161'))
if conf.exists('location'):
snmp['location'] = conf.return_value('location')
@@ -579,6 +585,24 @@ def verify(snmp):
if not os.path.isfile('/config/snmp/tls/certs/' + snmp['v3_tsm_key']):
raise ConfigError('TSM key must be fingerprint or filename in "/config/snmp/tls/certs/" folder')
+ for listen in snmp['listen_address']:
+ addr = listen[0]
+ port = listen[1]
+
+ if vyos.validate.is_ipv4(addr):
+ # example: udp:127.0.0.1:161
+ listen = 'udp:' + addr + ':' + port
+ else:
+ # example: udp6:[::1]:161
+ listen = 'udp6:' + '[' + addr + ']' + ':' + port
+
+ # We only wan't to configure addresses that exist on the system.
+ # Hint the user if they don't exist
+ if vyos.validate.is_addr_assigned(addr):
+ snmp['listen_on'].append(listen)
+ else:
+ print('WARNING: SNMP listen address {0} not configured!'.format(addr))
+
if 'v3_groups' in snmp.keys():
for group in snmp['v3_groups']:
#
@@ -641,48 +665,45 @@ def verify(snmp):
# Group must exist prior to mapping it into a group
# seclevel will be extracted from group
#
- error = True
if user['group']:
+ error = True
if 'v3_groups' in snmp.keys():
for group in snmp['v3_groups']:
if group['name'] == user['group']:
seclevel = group['seclevel']
error = False
- if error:
- raise ConfigError('You must create group "{0}" first'.format(user['group']))
+ if error:
+ raise ConfigError('You must create group "{0}" first'.format(user['group']))
# Depending on the configured security level
# the user has to provide additional info
- if seclevel in ('auth', 'priv'):
- if user['authPassword'] and user['authMasterKey']:
- raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for user auth')
+ if user['authPassword'] and user['authMasterKey']:
+ raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for user auth')
- if (not user['authPassword'] and not user['authMasterKey']):
- raise ConfigError('Must specify encrypted-key or plaintext-key for user auth')
+ if (not user['authPassword'] and not user['authMasterKey']):
+ raise ConfigError('Must specify encrypted-key or plaintext-key for user auth')
- # seclevel 'priv' is more restrictive
- if seclevel in ('priv'):
- if user['privPassword'] and user['privMasterKey']:
- raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for user privacy')
+ if user['privPassword'] and user['privMasterKey']:
+ raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for user privacy')
- if user['privPassword'] == '' and user['privMasterKey'] == '':
- raise ConfigError('Must specify encrypted-key or plaintext-key for user privacy')
+ if user['privPassword'] == '' and user['privMasterKey'] == '':
+ raise ConfigError('Must specify encrypted-key or plaintext-key for user privacy')
- if user['privMasterKey'] and user['engineID'] == '':
- raise ConfigError('Can not have "encrypted-key" without engineid')
+ if user['privMasterKey'] and user['engineID'] == '':
+ raise ConfigError('Can not have "encrypted-key" without engineid')
- if user['authPassword'] == '' and user['authMasterKey'] == '' and user['privTsmKey'] == '':
- raise ConfigError('Must specify auth or tsm-key for user auth')
+ if user['authPassword'] == '' and user['authMasterKey'] == '' and user['privTsmKey'] == '':
+ raise ConfigError('Must specify auth or tsm-key for user auth')
- if user['mode'] == '':
- raise ConfigError('Must specify user mode ro/rw')
+ if user['mode'] == '':
+ raise ConfigError('Must specify user mode ro/rw')
- if user['privTsmKey']:
- if not tsmKeyPattern.match(snmp['v3_tsm_key']):
- if not os.path.isfile('/etc/snmp/tls/certs/' + snmp['v3_tsm_key']):
- if not os.path.isfile('/config/snmp/tls/certs/' + snmp['v3_tsm_key']):
- raise ConfigError('User TSM key must be fingerprint or filename in "/config/snmp/tls/certs/" folder')
+ if user['privTsmKey']:
+ if not tsmKeyPattern.match(snmp['v3_tsm_key']):
+ if not os.path.isfile('/etc/snmp/tls/certs/' + snmp['v3_tsm_key']):
+ if not os.path.isfile('/config/snmp/tls/certs/' + snmp['v3_tsm_key']):
+ raise ConfigError('User TSM key must be fingerprint or filename in "/config/snmp/tls/certs/" folder')
if 'v3_views' in snmp.keys():
for view in snmp['v3_views']:
@@ -705,29 +726,35 @@ def generate(snmp):
return None
# Write client config file
- tmpl = jinja2.Template(client_config_tmpl, trim_blocks=True)
+ tmpl = jinja2.Template(client_config_tmpl)
config_text = tmpl.render(snmp)
with open(config_file_client, 'w') as f:
f.write(config_text)
# Write server config file
- tmpl = jinja2.Template(daemon_config_tmpl, trim_blocks=True)
+ tmpl = jinja2.Template(daemon_config_tmpl)
config_text = tmpl.render(snmp)
with open(config_file_daemon, 'w') as f:
f.write(config_text)
# Write access rights config file
- tmpl = jinja2.Template(access_config_tmpl, trim_blocks=True)
+ tmpl = jinja2.Template(access_config_tmpl)
config_text = tmpl.render(snmp)
with open(config_file_access, 'w') as f:
f.write(config_text)
# Write access rights config file
- tmpl = jinja2.Template(user_config_tmpl, trim_blocks=True)
+ tmpl = jinja2.Template(user_config_tmpl)
config_text = tmpl.render(snmp)
with open(config_file_user, 'w') as f:
f.write(config_text)
+ # Write init config file
+ tmpl = jinja2.Template(init_config_tmpl)
+ config_text = tmpl.render(snmp)
+ with open(config_file_init, 'w') as f:
+ f.write(config_text)
+
return None
def apply(snmp):
@@ -761,9 +788,17 @@ def apply(snmp):
# start SNMP daemon
os.system("sudo systemctl restart snmpd.service")
- # the passwords are not available immediately so this is a workaround
- # and should be changed to polling
- time.sleep(2)
+ # Passwords are not available immediately in the configuration file,
+ # after daemon startup - we wait until they have been processed by
+ # snmpd, which we see when a magic line appears in this file.
+ snmpReady = False
+ while not snmpReady:
+ with open(config_file_user, 'r') as f:
+ for line in f:
+ # Search for our magic string inside the file
+ if '**** DO NOT EDIT THIS FILE ****' in line:
+ snmpReady = True
+ break
# Back in the Perl days the configuration was re-read and any
# plaintext password inside the configuration was replaced by
@@ -795,6 +830,9 @@ def apply(snmp):
os.system('vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_delete service snmp v3 user "{0}" auth plaintext-key > /dev/null'.format(cfg['user']))
os.system('vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_delete service snmp v3 user "{0}" privacy plaintext-key > /dev/null'.format(cfg['user']))
+ # Enable AgentX in FRR
+ os.system('vtysh -c "configure terminal" -c "agentx"')
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py
index f1ac19473..2a5cba99a 100755
--- a/src/conf_mode/ssh.py
+++ b/src/conf_mode/ssh.py
@@ -67,82 +67,103 @@ UseDNS {{ host_validation }}
# Specifies the port number that sshd listens on. The default is 22.
# Multiple options of this type are permitted.
+{% if mport|length != 0 %}
+{% for p in mport %}
+Port {{ p }}
+{% endfor %}
+{% else %}
Port {{ port }}
+{% endif %}
# Gives the verbosity level that is used when logging messages from sshd
LogLevel {{ log_level }}
# Specifies whether root can log in using ssh
-PermitRootLogin {{ allow_root }}
+PermitRootLogin no
# Specifies whether password authentication is allowed
PasswordAuthentication {{ password_authentication }}
-{% if listen_on -%}
+{% if listen_on %}
# Specifies the local addresses sshd should listen on
-{% for a in listen_on -%}
+{% for a in listen_on %}
ListenAddress {{ a }}
-{% endfor -%}
+{% endfor %}
+{{ "\n" }}
{% endif %}
-{% if ciphers -%}
+{%- if ciphers %}
# Specifies the ciphers allowed. Multiple ciphers must be comma-separated.
#
# NOTE: As of now, there is no 'multi' node for 'ciphers', thus we have only one :/
Ciphers {{ ciphers | join(",") }}
+{{ "\n" }}
{% endif %}
-{% if mac -%}
+{%- if mac %}
# Specifies the available MAC (message authentication code) algorithms. The MAC
# algorithm is used for data integrity protection. Multiple algorithms must be
# comma-separated.
#
# NOTE: As of now, there is no 'multi' node for 'mac', thus we have only one :/
MACs {{ mac | join(",") }}
+{{ "\n" }}
{% endif %}
-{% if key_exchange -%}
+{%- if key_exchange %}
# Specifies the available KEX (Key Exchange) algorithms. Multiple algorithms must
# be comma-separated.
#
# NOTE: As of now, there is no 'multi' node for 'key-exchange', thus we have only one :/
KexAlgorithms {{ key_exchange | join(",") }}
+{{ "\n" }}
{% endif %}
-{% if allow_users -%}
+{%- if allow_users %}
# This keyword can be followed by a list of user name patterns, separated by spaces.
# If specified, login is allowed only for user names that match one of the patterns.
# Only user names are valid, a numerical user ID is not recognized.
AllowUsers {{ allow_users | join(" ") }}
+{{ "\n" }}
{% endif %}
-{% if allow_groups -%}
+{%- if allow_groups %}
# This keyword can be followed by a list of group name patterns, separated by spaces.
# If specified, login is allowed only for users whose primary group or supplementary
# group list matches one of the patterns. Only group names are valid, a numerical group
# ID is not recognized.
AllowGroups {{ allow_groups | join(" ") }}
+{{ "\n" }}
{% endif %}
-{% if deny_users -%}
+{%- if deny_users %}
# This keyword can be followed by a list of user name patterns, separated by spaces.
# Login is disallowed for user names that match one of the patterns. Only user names
# are valid, a numerical user ID is not recognized.
DenyUsers {{ deny_users | join(" ") }}
+{{ "\n" }}
{% endif %}
-{% if deny_groups -%}
+{%- if deny_groups %}
# This keyword can be followed by a list of group name patterns, separated by spaces.
# Login is disallowed for users whose primary group or supplementary group list matches
# one of the patterns. Only group names are valid, a numerical group ID is not recognized.
DenyGroups {{ deny_groups | join(" ") }}
+{{ "\n" }}
+{% endif %}
+
+{%- if client_keepalive %}
+# Sets a timeout interval in seconds after which if no data has been received from the client,
+# sshd will send a message through the encrypted channel to request a response from the client.
+# The default is 0, indicating that these messages will not be sent to the client.
+# This option applies to protocol version 2 only.
+ClientAliveInterval {{ client_keepalive }}
{% endif %}
"""
default_config_data = {
'port' : '22',
'log_level': 'INFO',
- 'allow_root': 'no',
'password_authentication': 'yes',
'host_validation': 'yes'
}
@@ -171,9 +192,6 @@ def get_config():
deny_groups = conf.return_values('access-control deny group')
ssh['deny_groups'] = deny_groups
- if conf.exists('allow-root'):
- ssh['allow-root'] = 'yes'
-
if conf.exists('ciphers'):
ciphers = conf.return_values('ciphers')
ssh['ciphers'] = ciphers
@@ -208,8 +226,17 @@ def get_config():
ssh['mac'] = mac
if conf.exists('port'):
- port = conf.return_value('port')
- ssh['port'] = port
+ ports = conf.return_values('port')
+ mport = []
+
+ for prt in ports:
+ mport.append(prt)
+
+ ssh['mport'] = mport
+
+ if conf.exists('client-keepalive-interval'):
+ client_keepalive = conf.return_value('client-keepalive-interval')
+ ssh['client_keepalive'] = client_keepalive
return ssh
@@ -228,7 +255,7 @@ def generate(ssh):
if ssh is None:
return None
- tmpl = jinja2.Template(config_tmpl)
+ tmpl = jinja2.Template(config_tmpl, trim_blocks=True)
config_text = tmpl.render(ssh)
with open(config_file, 'w') as f:
f.write(config_text)
@@ -236,10 +263,10 @@ def generate(ssh):
def apply(ssh):
if ssh is not None and 'port' in ssh.keys():
- os.system("sudo systemctl restart ssh")
+ os.system("sudo systemctl restart ssh.service")
else:
# SSH access is removed in the commit
- os.system("sudo systemctl stop ssh")
+ os.system("sudo systemctl stop ssh.service")
os.unlink(config_file)
return None
diff --git a/src/conf_mode/syslog.py b/src/conf_mode/syslog.py
index 5dfc6f390..f652cf3d0 100755
--- a/src/conf_mode/syslog.py
+++ b/src/conf_mode/syslog.py
@@ -93,7 +93,7 @@ def get_config():
config_data['files'].update(
{
'global' : {
- 'log-file' : '/var/log/vyos-rsyslog',
+ 'log-file' : '/var/log/messages',
'max-size' : 262144,
'action-on-max-size' : '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog',
'selectors' : '*.notice;local7.debug',
@@ -229,6 +229,18 @@ def generate(c):
f.write(config_text)
def verify(c):
+ #
+ # /etc/rsyslog.conf is generated somewhere and copied over the original (exists in /opt/vyatta/etc/rsyslog.conf)
+ # it interferes with the global logging, to make sure we are using a single base, template is enforced here
+ #
+
+ if not os.path.islink('/etc/rsyslog.conf'):
+ os.remove('/etc/rsyslog.conf')
+ os.symlink('/usr/share/vyos/templates/rsyslog/rsyslog.conf', '/etc/rsyslog.conf')
+
+ # /var/log/vyos-rsyslog were the old files, we may want to clean those up, but currently there
+ # is a chance that someone still needs it, so I don't automatically remove them
+
if c == None:
return None
diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py
new file mode 100755
index 000000000..0984b4545
--- /dev/null
+++ b/src/conf_mode/tftp_server.py
@@ -0,0 +1,155 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import sys
+import os
+import stat
+import pwd
+
+import jinja2
+import vyos.validate
+
+from vyos.config import Config
+from vyos import ConfigError
+
+config_file = r'/etc/default/tftpd-hpa'
+
+# Please be careful if you edit the template.
+config_tmpl = """
+### Autogenerated by tftp_server.py ###
+
+# See manual at https://linux.die.net/man/8/tftpd
+
+TFTP_USERNAME="tftp"
+TFTP_DIRECTORY="{{ directory }}"
+{% if listen_ipv4 and listen_ipv6 -%}
+TFTP_ADDRESS="{% for a in listen_ipv4 -%}{{ a }}:{{ port }}{{- " --address " if not loop.last -}}{% endfor -%} {% for a in listen_ipv6 %} --address [{{ a }}]:{{ port }}{% endfor -%}"
+{% elif listen_ipv4 -%}
+TFTP_ADDRESS="{% for a in listen_ipv4 -%}{{ a }}:{{ port }}{{- " --address " if not loop.last -}}{% endfor %} -4"
+{% elif listen_ipv6 -%}
+TFTP_ADDRESS="{% for a in listen_ipv6 -%}[{{ a }}]:{{ port }}{{- " --address " if not loop.last -}}{% endfor %} -6"
+{%- endif %}
+
+TFTP_OPTIONS="--secure {% if allow_upload %}--create --umask 000{% endif %}"
+
+"""
+
+default_config_data = {
+ 'directory': '',
+ 'allow_upload': False,
+ 'port': '69',
+ 'listen_ipv4': [],
+ 'listen_ipv6': []
+}
+
+def get_config():
+ tftpd = default_config_data
+ conf = Config()
+ if not conf.exists('service tftp-server'):
+ return None
+ else:
+ conf.set_level('service tftp-server')
+
+ if conf.exists('directory'):
+ tftpd['directory'] = conf.return_value('directory')
+
+ if conf.exists('allow-upload'):
+ tftpd['allow_upload'] = True
+
+ if conf.exists('port'):
+ tftpd['port'] = conf.return_value('port')
+
+ if conf.exists('listen-address'):
+ for addr in conf.return_values('listen-address'):
+ if vyos.validate.is_ipv4(addr):
+ tftpd['listen_ipv4'].append(addr)
+ else:
+ tftpd['listen_ipv6'].append(addr)
+
+ return tftpd
+
+def verify(tftpd):
+ # bail out early - looks like removal from running config
+ if tftpd is None:
+ return None
+
+ # Configuring allowed clients without a server makes no sense
+ if not tftpd['directory']:
+ raise ConfigError('TFTP root directory must be configured!')
+
+ if not (tftpd['listen_ipv4'] or tftpd['listen_ipv6']):
+ raise ConfigError('TFTP server listen address must be configured!')
+
+ for addr in tftpd['listen_ipv4']:
+ # we always bind to localhost
+ if '127.0.0.1' not in tftpd['listen_ipv4']:
+ tftpd['listen_ipv4'].append('127.0.0.1')
+
+ if not vyos.validate.is_addr_assigned(addr):
+ print('WARNING: TFTP server listen address {0} not configured!'.format(addr))
+
+ for addr in tftpd['listen_ipv6']:
+ # we always bind to localhost
+ if '::1' not in tftpd['listen_ipv6']:
+ tftpd['listen_ipv6'].append('::1')
+
+ if not vyos.validate.is_addr_assigned(addr):
+ print('WARNING: TFTP server listen address {0} not configured!'.format(addr))
+
+ return None
+
+def generate(tftpd):
+ # bail out early - looks like removal from running config
+ if tftpd is None:
+ return None
+
+ tmpl = jinja2.Template(config_tmpl)
+ config_text = tmpl.render(tftpd)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
+
+ return None
+
+def apply(tftpd):
+ if tftpd is not None:
+
+ tftp_root = tftpd['directory']
+ if not os.path.exists(tftp_root):
+ os.makedirs(tftp_root)
+ os.chmod(tftp_root, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH)
+ # get UNIX uid for user 'tftp'
+ tftp_uid = pwd.getpwnam('tftp').pw_uid
+ os.chown(tftp_root, tftp_uid, -1)
+
+ os.system('sudo systemctl restart tftpd-hpa.service')
+ else:
+ # TFTP server support is removed in the commit
+ os.system('sudo systemctl stop tftpd-hpa.service')
+ os.unlink(config_file)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py
index d21e3ef40..c458e3b04 100755
--- a/src/conf_mode/vrrp.py
+++ b/src/conf_mode/vrrp.py
@@ -78,6 +78,7 @@ vrrp_instance {{ group.name }} {
{% if group.use_vmac -%}
use_vmac {{group.interface}}v{{group.vrid}}
+ vmac_xmit_base
{% endif -%}
{% if group.auth_password -%}
@@ -183,7 +184,7 @@ def get_config():
if not group["priority"]:
group["priority"] = 100
if not group["preempt_delay"]:
- group["preempt_delay"] = 5 * 60
+ group["preempt_delay"] = 0
if not group["health_check_interval"]:
group["health_check_interval"] = 60
if not group["health_check_count"]:
@@ -273,7 +274,7 @@ def verify(data):
count = len(_groups) - 1
index = 0
while (index < count):
- if _groups[index]["vrid"] == _groups[index + 1]["vrid"]:
+ if (_groups[index]["vrid"] == _groups[index + 1]["vrid"]) and (_groups[index]["interface"] == _groups[index + 1]["interface"]):
raise ConfigError("VRID {0} is used in groups {1} and {2} that both use interface {3}. Groups on the same interface must use different VRIDs".format(
_groups[index]["vrid"], _groups[index]["name"], _groups[index + 1]["name"], _groups[index]["interface"]))
else:
diff --git a/src/conf_mode/wireguard.py b/src/conf_mode/wireguard.py
index e1c076e2a..c6440ad81 100755
--- a/src/conf_mode/wireguard.py
+++ b/src/conf_mode/wireguard.py
@@ -26,101 +26,101 @@ from vyos.config import Config
from vyos import ConfigError
dir = r'/config/auth/wireguard'
-pk = dir + '/private.key'
+pk = dir + '/private.key'
pub = dir + '/public.key'
+psk_file = r'/tmp/psk'
-### check_kmod may be removed in the future,
-### just want to have everything smoothly running after reboot
def check_kmod():
if not os.path.exists('/sys/module/wireguard'):
- sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod")
+ sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod")
if os.system('sudo modprobe wireguard') != 0:
sl.syslog(sl.LOG_NOTICE, "modprobe wireguard failed")
raise ConfigError("modprobe wireguard failed")
def get_config():
- config_data = {
- 'interfaces' : {}
- }
-
c = Config()
if not c.exists('interfaces wireguard'):
return None
-
- c.set_level('interfaces')
+
+ c.set_level('interfaces')
intfcs = c.list_nodes('wireguard')
intfcs_eff = c.list_effective_nodes('wireguard')
- new_lst = list( set(intfcs) - set(intfcs_eff) )
- del_lst = list( set(intfcs_eff) - set(intfcs) )
+ new_lst = list(set(intfcs) - set(intfcs_eff))
+ del_lst = list(set(intfcs_eff) - set(intfcs))
- ### setting deafult and determine status of the config
+ config_data = {
+ 'interfaces' : {}
+ }
+ ### setting defaults and determine status of the config
for intfc in intfcs:
cnf = 'wireguard ' + intfc
# default data struct
- config_data['interfaces'].update (
- {
- intfc : {
- 'addr' : '',
- 'descr' : intfc, ## snmp ifAlias
- 'lport' : '',
- 'status' : 'exists',
- 'state' : 'enabled',
- 'mtu' : 1420,
- 'peer' : {}
- }
+ config_data['interfaces'].update(
+ {
+ intfc : {
+ 'addr' : '',
+ 'descr' : intfc, ## snmp ifAlias
+ 'lport' : '',
+ 'status' : 'exists',
+ 'state' : 'enabled',
+ 'mtu' : '1420',
+ 'peer' : {}
+ }
}
- )
+ )
+ ### determine status either delete or create
for i in new_lst:
- config_data['interfaces'][i]['status'] = 'create'
+ config_data['interfaces'][i]['status'] = 'create'
for i in del_lst:
- config_data['interfaces'].update (
- {
- i : {
- 'status': 'delete'
+ config_data['interfaces'].update(
+ {
+ i : {
+ 'status': 'delete'
+ }
}
- }
)
- ### based on the status, set real values
+ ### based on the status, setup conf values
for intfc in intfcs:
cnf = 'wireguard ' + intfc
if config_data['interfaces'][intfc]['status'] != 'delete':
- #### addresses
+ ### addresses
if c.exists(cnf + ' address'):
config_data['interfaces'][intfc]['addr'] = c.return_values(cnf + ' address')
### listen port
- if c.exists(cnf + ' listen-port'):
- config_data['interfaces'][intfc]['lport'] = c.return_value(cnf + ' listen-port')
+ if c.exists(cnf + ' port'):
+ config_data['interfaces'][intfc]['lport'] = c.return_value(cnf + ' port')
### description
if c.exists(cnf + ' description'):
config_data['interfaces'][intfc]['descr'] = c.return_value(cnf + ' description')
### mtu
if c.exists(cnf + ' mtu'):
config_data['interfaces'][intfc]['mtu'] = c.return_value(cnf + ' mtu')
-
### peers
if c.exists(cnf + ' peer'):
for p in c.list_nodes(cnf + ' peer'):
- config_data['interfaces'][intfc]['peer'].update (
- {
- p : {
- 'allowed-ips' : [],
- 'endpoint' : ''
+ config_data['interfaces'][intfc]['peer'].update(
+ {
+ p : {
+ 'allowed-ips' : [],
+ 'endpoint' : '',
+ 'pubkey' : ''
+ }
}
- }
)
+ if c.exists(cnf + ' peer ' + p + ' pubkey'):
+ config_data['interfaces'][intfc]['peer'][p]['pubkey'] = c.return_value(cnf + ' peer ' + p + ' pubkey')
if c.exists(cnf + ' peer ' + p + ' allowed-ips'):
config_data['interfaces'][intfc]['peer'][p]['allowed-ips'] = c.return_values(cnf + ' peer ' + p + ' allowed-ips')
if c.exists(cnf + ' peer ' + p + ' endpoint'):
config_data['interfaces'][intfc]['peer'][p]['endpoint'] = c.return_value(cnf + ' peer ' + p + ' endpoint')
+ if c.exists(cnf + ' peer ' + p + ' persistent-keepalive'):
+ config_data['interfaces'][intfc]['peer'][p]['persistent-keepalive'] = c.return_value(cnf + ' peer ' + p + ' persistent-keepalive')
+ if c.exists(cnf + ' peer ' + p + ' preshared-key'):
+ config_data['interfaces'][intfc]['peer'][p]['psk'] = c.return_value(cnf + ' peer ' + p + ' preshared-key')
- ### persistent-keepalive
- if c.exists(cnf + ' peer ' + p + ' persistent-keepalive'):
- config_data['interfaces'][intfc]['peer'][p]['persistent-keepalive'] = c.return_value(cnf + ' peer ' + p + ' persistent-keepalive')
-
- #print (config_data)
return config_data
def verify(c):
@@ -130,34 +130,31 @@ def verify(c):
for i in c['interfaces']:
if c['interfaces'][i]['status'] != 'delete':
if not c['interfaces'][i]['addr']:
- raise ConfigError("address required for interface " + i)
- if not c['interfaces'][i]['lport']:
- raise ConfigError("listen-port required for interface " + i)
+ raise ConfigError("address required for interface " + i)
if not c['interfaces'][i]['peer']:
raise ConfigError("peer required on interface " + i)
- else:
- for p in c['interfaces'][i]['peer']:
- if not c['interfaces'][i]['peer'][p]['allowed-ips']:
- raise ConfigError("allowed-ips required on interface " + i + " for peer " + p)
- ### eventually check allowed-ips (if it's an ip and valid CIDR or so)
- ### endpoint needs to be IP:port
+ for p in c['interfaces'][i]['peer']:
+ if not c['interfaces'][i]['peer'][p]['allowed-ips']:
+ raise ConfigError("allowed-ips required on interface " + i + " for peer " + p)
+ if not c['interfaces'][i]['peer'][p]['pubkey']:
+ raise ConfigError("pubkey from your peer is mandatory on " + i + " for peer " + p)
+
def apply(c):
### no wg config left, delete all wireguard devices on the os
if not c:
net_devs = os.listdir('/sys/class/net/')
for dev in net_devs:
- buf = open('/sys/class/net/' + dev + '/uevent','r').read()
+ buf = open('/sys/class/net/' + dev + '/uevent', 'r').read()
if re.search("DEVTYPE=wireguard", buf, re.I|re.M):
- wg_intf = re.sub("INTERFACE=","", re.search("INTERFACE=.*", buf, re.I|re.M).group(0) )
+ wg_intf = re.sub("INTERFACE=", "", re.search("INTERFACE=.*", buf, re.I|re.M).group(0))
sl.syslog(sl.LOG_NOTICE, "removing interface " + wg_intf)
subprocess.call(['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True)
return None
-
+
###
- ## to find the diffs between old config an new config
- ## so we only configure/delete what was not previously configured
+ ## find the diffs between effective config an new config
###
c_eff = Config()
c_eff.set_level('interfaces wireguard')
@@ -175,92 +172,139 @@ def apply(c):
subprocess.call(['ip l a dev ' + intf + ' type wireguard 2>/dev/null'], shell=True)
for addr in c['interfaces'][intf]['addr']:
- add_addr(intf, addr)
- configure_interface(c,intf)
- subprocess.call(['ip l set up dev ' + intf + ' &>/dev/null'], shell=True)
+ add_addr(intf, addr)
+
+ subprocess.call(['ip l set up dev ' + intf + ' mtu ' + c['interfaces'][intf]['mtu'] + ' &>/dev/null'], shell=True)
+ configure_interface(c, intf)
### config updates
if c['interfaces'][intf]['status'] == 'exists':
### IP address change
- addr_eff = re.sub("\'", "", c_eff.return_effective_values(intf + ' address')).split()
- addr_rem = list( set(addr_eff) - set(c['interfaces'][intf]['addr']) )
- addr_add = list( set(c['interfaces'][intf]['addr']) - set(addr_eff) )
+ addr_eff = re.sub("\'", "", c_eff.return_effective_values(intf + ' address')).split()
+ addr_rem = list(set(addr_eff) - set(c['interfaces'][intf]['addr']))
+ addr_add = list(set(c['interfaces'][intf]['addr']) - set(addr_eff))
- if len(addr_rem) !=0:
+ if len(addr_rem) != 0:
for addr in addr_rem:
del_addr(intf, addr)
- if len(addr_add) !=0:
+ if len(addr_add) != 0:
for addr in addr_add:
add_addr(intf, addr)
- ### persistent-keepalive
+ ## mtu update
+ mtu = c['interfaces'][intf]['mtu']
+ if mtu != 1420:
+ sl.syslog(sl.LOG_NOTICE, "setting mtu to " + mtu + " on " + intf)
+ subprocess.call(['ip l set mtu ' + mtu + ' dev ' + intf + ' &>/dev/null'], shell=True)
+
+ ### persistent-keepalive
for p in c_eff.list_nodes(intf + ' peer'):
val_eff = ""
- val = ""
+ val = ""
if c_eff.exists_effective(intf + ' peer ' + p + ' persistent-keepalive'):
val_eff = c_eff.return_effective_value(intf + ' peer ' + p + ' persistent-keepalive')
if 'persistent-keepalive' in c['interfaces'][intf]['peer'][p]:
val = c['interfaces'][intf]['peer'][p]['persistent-keepalive']
-
+
### disable keepalive
if val_eff and not val:
- c['interfaces'][intf]['peer'][p]['persistent-keepalive'] = 0
-
- ### set ne keepalive value
+ 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
-
- ## config == effective config, no change
- if val_eff == val:
- del c['interfaces'][intf]['peer'][p]['persistent-keepalive']
## wg command call
- configure_interface(c,intf)
+ configure_interface(c, intf)
- ### ifalias for snmp from description
+ ### ifalias for snmp from description
descr_eff = c_eff.return_effective_value(intf + ' description')
cnf_descr = c['interfaces'][intf]['descr']
if descr_eff != cnf_descr:
- open('/sys/class/net/' + str(intf) + '/ifalias','w').write(str(cnf_descr))
+ with open('/sys/class/net/' + str(intf) + '/ifalias', 'w') as fh:
+ fh.write(str(cnf_descr))
def configure_interface(c, intf):
for p in c['interfaces'][intf]['peer']:
- cmd = "wg set " + intf + \
- " listen-port " + c['interfaces'][intf]['lport'] + \
- " private-key " + pk + \
- " peer " + p
- cmd += " allowed-ips "
+ ## config init for wg call
+ wg_config = {
+ 'interface' : intf,
+ 'port' : 0,
+ 'private-key' : pk,
+ 'pubkey' : '',
+ 'psk' : '/dev/null',
+ 'allowed-ips' : [],
+ 'fwmark' : 0x00,
+ 'endpoint' : None,
+ 'keepalive' : 0
+ }
- for ap in c['interfaces'][intf]['peer'][p]['allowed-ips']:
- if ap != c['interfaces'][intf]['peer'][p]['allowed-ips'][-1]:
- cmd += ap + ","
- else:
- cmd += ap
+ ## mandatory settings
+ wg_config['pubkey'] = c['interfaces'][intf]['peer'][p]['pubkey']
+ wg_config['allowed-ips'] = c['interfaces'][intf]['peer'][p]['allowed-ips']
+
+ ## optional settings
+ # listen-port
+ if c['interfaces'][intf]['lport']:
+ wg_config['port'] = c['interfaces'][intf]['lport']
+
+ ## endpoint
+ if c['interfaces'][intf]['peer'][p]['endpoint']:
+ wg_config['endpoint'] = c['interfaces'][intf]['peer'][p]['endpoint']
+
+ ## persistent-keepalive
+ if 'persistent-keepalive' in c['interfaces'][intf]['peer'][p]:
+ wg_config['keepalive'] = c['interfaces'][intf]['peer'][p]['persistent-keepalive']
+
+ ## preshared-key - is only read from a file, it's called via sudo redirection doesn't work either
+ if 'psk' in c['interfaces'][intf]['peer'][p]:
+ old_umask = os.umask(0o077)
+ open(psk_file, 'w').write(str(c['interfaces'][intf]['peer'][p]['psk']))
+ os.umask(old_umask)
+ wg_config['psk'] = psk_file
+
+ ### assemble wg command
+ cmd = "sudo wg set " + intf
+ cmd += " listen-port " + str(wg_config['port'])
+ cmd += " private-key " + wg_config['private-key']
+ cmd += " peer " + wg_config['pubkey']
+ cmd += " preshared-key " + wg_config['psk']
+ cmd += " allowed-ips "
+ for ap in wg_config['allowed-ips']:
+ if ap != wg_config['allowed-ips'][-1]:
+ cmd += ap + ","
+ else:
+ cmd += ap
- ## endpoint is only required if wg runs as client
- if c['interfaces'][intf]['peer'][p]['endpoint']:
- cmd += " endpoint " + c['interfaces'][intf]['peer'][p]['endpoint']
+ if wg_config['endpoint']:
+ cmd += " endpoint " + wg_config['endpoint']
- if 'persistent-keepalive' in c['interfaces'][intf]['peer'][p]:
- cmd += " persistent-keepalive " + str( c['interfaces'][intf]['peer'][p]['persistent-keepalive'])
+ if wg_config['keepalive'] != 0:
+ cmd += " persistent-keepalive " + wg_config['keepalive']
+ else:
+ cmd += " persistent-keepalive 0"
- sl.syslog(sl.LOG_NOTICE, "sudo " + cmd)
- subprocess.call([ 'sudo ' + cmd], shell=True)
+ sl.syslog(sl.LOG_NOTICE, cmd)
+ #print (cmd)
+ subprocess.call([cmd], shell=True)
+ """ remove psk_file """
+ if os.path.exists(psk_file):
+ os.remove(psk_file)
def add_addr(intf, addr):
ret = subprocess.call(['ip a a dev ' + intf + ' ' + addr + ' &>/dev/null'], shell=True)
if ret != 0:
- raise ConfigError('Can\'t set IP ' + addr + ' on ' + intf )
+ raise ConfigError('Can\'t set IP ' + addr + ' on ' + intf)
else:
sl.syslog(sl.LOG_NOTICE, "ip a a dev " + intf + " " + addr)
def del_addr(intf, addr):
ret = subprocess.call(['ip a d dev ' + intf + ' ' + addr + ' &>/dev/null'], shell=True)
if ret != 0:
- raise ConfigError('Can\'t delete IP ' + addr + ' on ' + intf )
+ raise ConfigError('Can\'t delete IP ' + addr + ' on ' + intf)
else:
sl.syslog(sl.LOG_NOTICE, "ip a d dev " + intf + " " + addr)
@@ -269,9 +313,7 @@ if __name__ == '__main__':
check_kmod()
c = get_config()
verify(c)
- #generate(c)
apply(c)
except ConfigError as e:
print(e)
sys.exit(1)
-