summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/bcast_relay.py1
-rwxr-xr-xsrc/conf_mode/dhcp_server.py64
-rwxr-xr-xsrc/conf_mode/dhcpv6_server.py451
-rwxr-xr-xsrc/conf_mode/mdns_repeater.py92
-rwxr-xr-xsrc/conf_mode/ntp.py2
-rwxr-xr-xsrc/conf_mode/snmp.py206
-rwxr-xr-xsrc/conf_mode/syslog.py14
-rwxr-xr-xsrc/conf_mode/tftp_server.py12
-rwxr-xr-xsrc/conf_mode/wireguard.py37
9 files changed, 711 insertions, 168 deletions
diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py
index 8cc948610..d1257d4a5 100755
--- a/src/conf_mode/bcast_relay.py
+++ b/src/conf_mode/bcast_relay.py
@@ -19,7 +19,6 @@
import sys
import os
import fnmatch
-import subprocess
import jinja2
from vyos.config import Config
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
index 1458ed1d0..2a2b1fe6c 100755
--- a/src/conf_mode/dhcp_server.py
+++ b/src/conf_mode/dhcp_server.py
@@ -38,8 +38,8 @@ config_tmpl = """
# For options please consult the following website:
# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html
-
-log-facility local7;
+#
+# log-facility local7;
{% if hostfile_update %}
on commit {
@@ -112,12 +112,12 @@ failover peer "{{ subnet.failover_name }}" {
{% for network in shared_network %}
{%- if not network.disabled -%}
shared-network {{ network.name }} {
- {% if network.authoritative %}authoritative;{% endif %}
+ {{ "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 -%}
+ {%- endfor %}
{%- endif %}
{%- for subnet in network.subnet %}
subnet {{ subnet.address }} netmask {{ subnet.netmask }} {
@@ -195,9 +195,19 @@ shared-network {{ network.name }} {
}
{%- 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 }}"; }
@@ -606,17 +616,17 @@ def verify(dhcp):
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']))
- # Inspect our subnet configuration
- failover_names = []
- listen_ok = False
- subnets = []
- for network in dhcp['shared_network']:
for subnet in network['subnet']:
# Subnet static route declaration requires destination and router
if subnet['static_subnet'] or subnet['static_router']:
@@ -650,34 +660,34 @@ def verify(dhcp):
stop = range['stop']
# DHCP stop IP required after start IP
if start and not stop:
- raise ConfigError('Stop IP address in DHCP range for start {0} is not defined!'.format(start))
+ 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('Start IP address {0} of DHCP range is not in subnet {1}\n' \
+ 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('Stop IP address {0} of DHCP range is not in subnet {1}\n' \
+ 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('Stop IP address {0} of DHCP range should be greater or equal\n' \
- 'to the start IP address {1} of this range!'.format(stop, 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 IP address {0} defined multipe times!'.format(range['start']))
+ '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 IP address {0} defined multipe times!'.format(range['stop']))
+ 'Pool stop address {0} defined multipe times!'.format(stop))
else:
range_stop.append(stop)
@@ -705,18 +715,18 @@ def verify(dhcp):
for mapping in subnet['static_mapping']:
# Static IP address must be configured
if not mapping['ip_address']:
- raise ConfigError('No static lease IP address specified for static mapping {0}\n' \
- 'under shared network name {1}!'.format(mapping['name'], network['name']))
+ 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('Static DHCP lease IP address {0} under static mapping {1}\n' \
- 'in shared network {2} is outside DHCP lease network {3}!' \
+ 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('No static lease MAC address specified for static mapping\n' \
+ 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.
@@ -725,28 +735,24 @@ def verify(dhcp):
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('Subnets must be unique! Subnet {0} defined multiple times!'.format(subnet))
+ 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.compare_networks(net2) != 0):
+ if (net != net2):
if net.overlaps(net2):
- raise ConfigError('Conflicting subnet ranges: {0} overlaps with {1}'.format(net, 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')
+ 'DHCP server listens on a one broadcast interface!')
return None
diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py
new file mode 100755
index 000000000..bb3e6e90d
--- /dev/null
+++ b/src/conf_mode/dhcpv6_server.py
@@ -0,0 +1,451 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import sys
+import os
+import ipaddress
+
+import jinja2
+
+import vyos.validate
+
+from vyos.config import Config
+from vyos import ConfigError
+
+config_file = r'/etc/dhcp/dhcpd6.conf'
+lease_file = r'/config/dhcpd6.leases'
+daemon_config_file = r'/etc/default/isc-dhcpv6-server'
+
+# Please be careful if you edit the template.
+config_tmpl = """
+### Autogenerated by dhcpv6_server.py ###
+
+# For options please consult the following website:
+# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html
+
+log-facility local7;
+{%- if preference %}
+option dhcp6.preference {{ preference }};
+{%- endif %}
+
+# Shared network configration(s)
+{% for network in shared_network %}
+{%- if not network.disabled -%}
+shared-network {{ network.name }} {
+ {%- for subnet in network.subnet %}
+ subnet6 {{ subnet.network }} {
+ {%- for range in subnet.range6_prefix %}
+ range6 {{ range.prefix }}{{ " temporary" if range.temporary }};
+ {%- endfor %}
+ {%- for range in subnet.range6 %}
+ range6 {{ range.start }} {{ range.stop }};
+ {%- endfor %}
+ {%- if subnet.domain_search %}
+ option dhcp6.domain-search {{ subnet.domain_search | join(', ') }};
+ {%- endif %}
+ {%- if subnet.lease_def %}
+ default-lease-time {{ subnet.lease_def }};
+ {%- endif %}
+ {%- if subnet.lease_max %}
+ max-lease-time {{ subnet.lease_max }};
+ {%- endif %}
+ {%- if subnet.lease_min %}
+ min-lease-time {{ subnet.lease_min }};
+ {%- endif %}
+ {%- if subnet.dns_server %}
+ option dhcp6.name-servers {{ subnet.dns_server | join(', ') }};
+ {%- endif %}
+ {%- if subnet.nis_domain %}
+ option dhcp6.nis-domain-name "{{ subnet.nis_domain }}";
+ {%- endif %}
+ {%- if subnet.nis_server %}
+ option dhcp6.nis-servers {{ subnet.nis_server | join(', ') }};
+ {%- endif %}
+ {%- if subnet.nisp_domain %}
+ option dhcp6.nisp-domain-name "{{ subnet.nisp_domain }}";
+ {%- endif %}
+ {%- if subnet.nisp_server %}
+ option dhcp6.nisp-servers {{ subnet.nisp_server | join(', ') }};
+ {%- endif %}
+ {%- if subnet.sip_address %}
+ option dhcp6.sip-servers-addresses {{ subnet.sip_address | join(', ') }};
+ {%- endif %}
+ {%- if subnet.sip_hostname %}
+ option dhcp6.sip-servers-names {{ subnet.sip_hostname | join(', ') }};
+ {%- endif %}
+ {%- if subnet.sntp_server %}
+ option dhcp6.sntp-servers {{ subnet.sntp_server | join(', ') }};
+ {%- endif %}
+ {%- for host in subnet.static_mapping %}
+ {% if not host.disabled -%}
+ host {{ network.name }}_{{ host.name }} {
+ host-identifier option dhcp6.client-id "{{ host.client_identifier }}";
+ fixed-address6 {{ host.ipv6_address }};
+ }
+ {%- endif %}
+ {%- endfor %}
+ }
+ {%- endfor %}
+}
+{%- endif %}
+{% endfor %}
+
+"""
+
+daemon_tmpl = """
+### Autogenerated by dhcp_server.py ###
+
+# sourced by /etc/init.d/isc-dhcpv6-server
+
+DHCPD_CONF=/etc/dhcp/dhcpd6.conf
+DHCPD_PID=/var/run/dhcpd6.pid
+OPTIONS="-6 -lf {{ lease_file }}"
+INTERFACES=""
+"""
+
+default_config_data = {
+ 'lease_file': lease_file,
+ 'preference': '',
+ 'disabled': False,
+ 'shared_network': []
+}
+
+def get_config():
+ dhcpv6 = default_config_data
+ conf = Config()
+ if not conf.exists('service dhcpv6-server'):
+ return None
+ else:
+ conf.set_level('service dhcpv6-server')
+
+ # Check for global disable of DHCPv6 service
+ if conf.exists('disable'):
+ dhcpv6['disabled'] = True
+ return dhcpv6
+
+ # Preference of this DHCPv6 server compared with others
+ if conf.exists('preference'):
+ dhcpv6['preference'] = conf.return_value('preference')
+
+ # check for multiple, shared networks served with DHCPv6 addresses
+ if conf.exists('shared-network-name'):
+ for network in conf.list_nodes('shared-network-name'):
+ conf.set_level('service dhcpv6-server shared-network-name {0}'.format(network))
+ config = {
+ 'name': network,
+ 'disabled': False,
+ 'subnet': []
+ }
+
+ # If disabled, the shared-network configuration becomes inactive
+ if conf.exists('disable'):
+ config['disabled'] = True
+
+ # check for multiple subnet configurations in a shared network
+ if conf.exists('subnet'):
+ for net in conf.list_nodes('subnet'):
+ conf.set_level('service dhcpv6-server shared-network-name {0} subnet {1}'.format(network, net))
+ subnet = {
+ 'network': net,
+ 'range6_prefix': [],
+ 'range6': [],
+ 'default_router': '',
+ 'dns_server': [],
+ 'domain_name': '',
+ 'domain_search': [],
+ 'lease_def': '',
+ 'lease_min': '',
+ 'lease_max': '',
+ 'nis_domain': '',
+ 'nis_server': [],
+ 'nisp_domain': '',
+ 'nisp_server': [],
+ 'sip_address': [],
+ 'sip_hostname': [],
+ 'sntp_server': [],
+ 'static_mapping': []
+ }
+
+ # For any subnet on which addresses will be assigned dynamically, there must be at
+ # least one address range statement. The range statement gives the lowest and highest
+ # IP addresses in a range. All IP addresses in the range should be in the subnet in
+ # which the range statement is declared.
+ if conf.exists('address-range prefix'):
+ for prefix in conf.list_nodes('address-range prefix'):
+ range = {
+ 'prefix': prefix,
+ 'temporary': False
+ }
+
+ # Address range will be used for temporary addresses
+ if conf.exists('address-range prefix {0} temporary'.format(range['prefix'])):
+ range['temporary'] = True
+
+ # Append to subnet temporary range6 list
+ subnet['range6_prefix'].append(range)
+
+ if conf.exists('address-range start'):
+ for range in conf.list_nodes('address-range start'):
+ range = {
+ 'start': range,
+ 'stop': conf.return_value('address-range start {0} stop'.format(range))
+ }
+
+ # Append to subnet range6 list
+ subnet['range6'].append(range)
+
+ # The domain-search option specifies a 'search list' of Domain Names to be used
+ # by the client to locate not-fully-qualified domain names.
+ if conf.exists('domain-search'):
+ for domain in conf.return_values('domain-search'):
+ subnet['domain_search'].append('"' + domain + '"')
+
+ # IPv6 address valid lifetime
+ # (at the end the address is no longer usable by the client)
+ # (set to 30 days, the usual IPv6 default)
+ if conf.exists('lease-time default'):
+ subnet['lease_def'] = conf.return_value('lease-time default')
+
+ # Time should be the maximum length in seconds that will be assigned to a lease.
+ # The only exception to this is that Dynamic BOOTP lease lengths, which are not
+ # specified by the client, are not limited by this maximum.
+ if conf.exists('lease-time maximum'):
+ subnet['lease_max'] = conf.return_value('lease-time maximum')
+
+ # Time should be the minimum length in seconds that will be assigned to a lease
+ if conf.exists('lease-time minimum'):
+ subnet['lease_min'] = conf.return_value('lease-time minimum')
+
+ # Specifies a list of Domain Name System name servers available to the client.
+ # Servers should be listed in order of preference.
+ if conf.exists('name-server'):
+ subnet['dns_server'] = conf.return_values('name-server')
+
+ # Ancient NIS (Network Information Service) domain name
+ if conf.exists('nis-domain'):
+ subnet['nis_domain'] = conf.return_value('nis-domain')
+
+ # Ancient NIS (Network Information Service) servers
+ if conf.exists('nis-server'):
+ subnet['nis_server'] = conf.return_values('nis-server')
+
+ # Ancient NIS+ (Network Information Service) domain name
+ if conf.exists('nisplus-domain'):
+ subnet['nisp_domain'] = conf.return_value('nisplus-domain')
+
+ # Ancient NIS+ (Network Information Service) servers
+ if conf.exists('nisplus-server'):
+ subnet['nisp_server'] = conf.return_values('nisplus-server')
+
+ # Prefix Delegation (RFC 3633)
+ if conf.exists('prefix-delegation'):
+ print('TODO: This option is actually not implemented right now!')
+
+ # Local SIP server that is to be used for all outbound SIP requests - IPv6 address
+ if conf.exists('sip-server-address'):
+ subnet['sip_address'] = conf.return_values('sip-server-address')
+
+ # Local SIP server that is to be used for all outbound SIP requests - hostname
+ if conf.exists('sip-server-name'):
+ for hostname in conf.return_values('sip-server-name'):
+ subnet['sip_hostname'].append('"' + hostname + '"')
+
+ # List of local SNTP servers available for the client to synchronize their clocks
+ if conf.exists('sntp-server'):
+ subnet['sntp_server'] = conf.return_values('sntp-server')
+
+ #
+ # Static DHCP v6 leases
+ #
+ if conf.exists('static-mapping'):
+ for mapping in conf.list_nodes('static-mapping'):
+ conf.set_level('service dhcpv6-server shared-network-name {0} subnet {1} static-mapping {2}'.format(network, net, mapping))
+ mapping = {
+ 'name': mapping,
+ 'disabled': False,
+ 'ipv6_address': '',
+ 'client_identifier': '',
+ }
+
+ # This static lease is disabled
+ if conf.exists('disable'):
+ mapping['disabled'] = True
+
+ # IPv6 address used for this DHCP client
+ if conf.exists('ipv6-address'):
+ mapping['ipv6_address'] = conf.return_value('ipv6-address')
+
+ # This option specifies the client’s DUID identifier. DUIDs are similar but different from DHCPv4 client identifiers
+ if conf.exists('identifier'):
+ mapping['client_identifier'] = conf.return_value('identifier')
+
+ # append static mapping configuration tu subnet list
+ subnet['static_mapping'].append(mapping)
+
+ # append subnet configuration to shared network subnet list
+ config['subnet'].append(subnet)
+
+
+ # append shared network configuration to config dictionary
+ dhcpv6['shared_network'].append(config)
+
+ return dhcpv6
+
+def verify(dhcpv6):
+ if dhcpv6 is None:
+ return None
+
+ if dhcpv6['disabled']:
+ return None
+
+ # If DHCP is enabled we need one share-network
+ if len(dhcpv6['shared_network']) == 0:
+ raise ConfigError('No DHCPv6 shared networks configured.\n' \
+ 'At least one DHCPv6 shared network must be configured.')
+
+ # Inspect shared-network/subnet
+ subnets = []
+ listen_ok = False
+
+ for network in dhcpv6['shared_network']:
+ # A shared-network requires a subnet definition
+ if len(network['subnet']) == 0:
+ raise ConfigError('No DHCPv6 lease subnets configured for {0}. At least one\n' \
+ 'lease subnet must be configured for each shared network.'.format(network['name']))
+
+ range6_start = []
+ range6_stop = []
+ for subnet in network['subnet']:
+ # Ususal range declaration with a start and stop address
+ for range6 in subnet['range6']:
+ # shorten names
+ start = range6['start']
+ stop = range6['stop']
+
+ # DHCPv6 stop address is required
+ if start and not stop:
+ raise ConfigError('DHCPv6 range stop address for start {0} is not defined!'.format(start))
+
+ # Start address must be inside network
+ if not ipaddress.ip_address(start) in ipaddress.ip_network(subnet['network']):
+ raise ConfigError('DHCPv6 range start address {0} is not in subnet {1}\n' \
+ 'specified for shared network {2}!'.format(start, subnet['network'], network['name']))
+
+ # Stop address must be inside network
+ if not ipaddress.ip_address(stop) in ipaddress.ip_network(subnet['network']):
+ raise ConfigError('DHCPv6 range stop address {0} is not in subnet {1}\n' \
+ 'specified for shared network {2}!'.format(stop, subnet['network'], network['name']))
+
+ # Stop address must be greater or equal to start address
+ if not ipaddress.ip_address(stop) >= ipaddress.ip_address(start):
+ raise ConfigError('DHCPv6 range stop address {0} must be greater or equal\n' \
+ 'to the range start address {1}!'.format(stop, start))
+
+ # DHCPv6 range start address must be unique - two ranges can't
+ # start with the same address - makes no sense
+ if start in range6_start:
+ raise ConfigError('Conflicting DHCPv6 lease range:\n' \
+ 'Pool start address {0} defined multipe times!'.format(start))
+ else:
+ range6_start.append(start)
+
+ # DHCPv6 range stop address must be unique - two ranges can't
+ # end with the same address - makes no sense
+ if stop in range6_stop:
+ raise ConfigError('Conflicting DHCPv6 lease range:\n' \
+ 'Pool stop address {0} defined multipe times!'.format(stop))
+ else:
+ range6_stop.append(stop)
+
+ # We also have prefixes that require checking
+ for prefix in subnet['range6_prefix']:
+ # If configured prefix does not match our subnet, we have to check that it's inside
+ if ipaddress.ip_network(prefix['prefix']) != ipaddress.ip_network(subnet['network']):
+ # Configured prefixes must be inside our network
+ if not ipaddress.ip_network(prefix['prefix']) in ipaddress.ip_network(subnet['network']):
+ raise ConfigError('DHCPv6 prefix {0} is not in subnet {1}\n' \
+ 'specified for shared network {2}!'.format(prefix['prefix'], subnet['network'], network['name']))
+
+ # DHCPv6 requires at least one configured address range or one static mapping
+ if not network['disabled']:
+ if vyos.validate.is_subnet_connected(subnet['network']):
+ listen_ok = True
+
+ # DHCPv6 subnet must not overlap. ISC DHCP also complains about overlapping
+ # subnets: "Warning: subnet 2001:db8::/32 overlaps subnet 2001:db8:1::/32"
+ net = ipaddress.ip_network(subnet['network'])
+ for n in subnets:
+ net2 = ipaddress.ip_network(n)
+ if (net != net2):
+ if net.overlaps(net2):
+ raise ConfigError('DHCPv6 conflicting subnet ranges: {0} overlaps {1}'.format(net, net2))
+
+ if not listen_ok:
+ raise ConfigError('None of the DHCPv6 subnets are connected to a subnet6 on\n' \
+ 'this machine. At least one subnet6 must be connected such that\n' \
+ 'DHCPv6 listens on an interface!')
+
+
+ return None
+
+def generate(dhcpv6):
+ if dhcpv6 is None:
+ return None
+
+ if dhcpv6['disabled']:
+ print('Warning: DHCPv6 server will be deactivated because it is disabled')
+ return None
+
+ tmpl = jinja2.Template(config_tmpl)
+ config_text = tmpl.render(dhcpv6)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
+
+ tmpl = jinja2.Template(daemon_tmpl)
+ config_text = tmpl.render(dhcpv6)
+ with open(daemon_config_file, 'w') as f:
+ f.write(config_text)
+
+ return None
+
+def apply(dhcpv6):
+ if (dhcpv6 is None) or dhcpv6['disabled']:
+ # DHCP server is removed in the commit
+ os.system('sudo systemctl stop isc-dhcpv6-server.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
+ if os.path.exists(daemon_config_file):
+ os.unlink(daemon_config_file)
+ else:
+ # If our file holding DHCPv6 leases does yet not exist - create it
+ if not os.path.exists(lease_file):
+ os.mknod(lease_file)
+
+ os.system('sudo systemctl restart isc-dhcpv6-server.service')
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/conf_mode/mdns_repeater.py b/src/conf_mode/mdns_repeater.py
index 474a6a5cf..cef735c0d 100755
--- a/src/conf_mode/mdns_repeater.py
+++ b/src/conf_mode/mdns_repeater.py
@@ -18,7 +18,7 @@
import sys
import os
-
+import jinja2
import netifaces
from vyos.config import Config
@@ -26,60 +26,78 @@ from vyos import ConfigError
config_file = r'/etc/default/mdns-repeater'
-def get_config():
- interface_list = []
+config_tmpl = """
+### Autogenerated by mdns_repeater.py ###
+DAEMON_ARGS="{{ interfaces | join(' ') }}"
+"""
+
+default_config_data = {
+ 'disabled': False,
+ 'interfaces': []
+}
+def get_config():
+ mdns = default_config_data
conf = Config()
- conf.set_level('service mdns repeater')
- if not conf.exists(''):
- return interface_list
+ if not conf.exists('service mdns repeater'):
+ return None
+ else:
+ conf.set_level('service mdns repeater')
- if conf.exists('interface'):
- intfs_names = []
- intfs_names = conf.return_values('interface')
+ # Service can be disabled by user
+ if conf.exists('disable'):
+ mdns['disabled'] = True
+ return mdns
- for name in intfs_names:
- interface_list.append(name)
+ # Interface to repeat mDNS advertisements
+ if conf.exists('interface'):
+ mdns['interfaces'] = conf.return_values('interface')
- return interface_list
+ return mdns
def verify(mdns):
- # '0' interfaces are possible, think of service deletion. Only '1' is not supported!
- if len(mdns) == 1:
- raise ConfigError('At least 2 interfaces must be specified but %d given!' % len(mdns))
-
- # For mdns-repeater to work it is essential that the interfaces
- # have an IP address assigned
- for intf in mdns:
- try:
- netifaces.ifaddresses(intf)[netifaces.AF_INET]
- except KeyError as e:
- raise ConfigError('No IP address configured for interface "%s"!' % intf)
+ if mdns is None:
+ return None
+
+ if mdns['disabled']:
+ return None
+
+ # We need at least two interfaces to repeat mDNS advertisments
+ if len(mdns['interfaces']) < 2:
+ raise ConfigError('mDNS repeater requires at least 2 configured interfaces!')
+
+ # For mdns-repeater to work it is essential that the interfaces has
+ # an IPv4 address assigned
+ for interface in mdns['interfaces']:
+ if netifaces.AF_INET in netifaces.ifaddresses(interface).keys():
+ if len(netifaces.ifaddresses(interface)[netifaces.AF_INET]) < 1:
+ raise ConfigError('mDNS repeater requires an IPv6 address configured on interface %s!'.format(interface))
return None
def generate(mdns):
- config_header = '### Autogenerated by mdns_repeater.py ###\n'
- if len(mdns) > 0:
- config_args = 'DAEMON_ARGS="' + ' '.join(str(e) for e in mdns) + '"\n'
- else:
- config_args = 'DAEMON_ARGS=""\n'
+ if mdns is None:
+ return None
+
+ if mdns['disabled']:
+ print('Warning: mDNS repeater will be deactivated because it is disabled')
+ return None
- # write new configuration file
- f = open(config_file, 'w')
- f.write(config_header)
- f.write(config_args)
- f.close()
+ tmpl = jinja2.Template(config_tmpl)
+ config_text = tmpl.render(mdns)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
return None
def apply(mdns):
- if len(mdns) == 0:
- cmd = "sudo systemctl stop mdns-repeater"
+ if (mdns is None) or mdns['disabled']:
+ os.system('sudo systemctl stop mdns-repeater')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
else:
- cmd = "sudo systemctl restart mdns-repeater"
+ os.system('sudo systemctl restart mdns-repeater')
- os.system(cmd)
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py
index 8533411cc..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
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py
index b98741913..69952e5e2 100755
--- a/src/conf_mode/snmp.py
+++ b/src/conf_mode/snmp.py
@@ -21,7 +21,6 @@ import os
import shutil
import stat
import pwd
-import time
import jinja2
import random
@@ -38,6 +37,7 @@ config_file_client = r'/etc/snmp/snmp.conf'
config_file_daemon = r'/etc/snmp/snmpd.conf'
config_file_access = r'/usr/share/snmp/snmpd.conf'
config_file_user = r'/var/lib/snmp/snmpd.conf'
+config_file_init = r'/etc/default/snmpd'
# SNMP OIDs used to mark auth/priv type
OIDs = {
@@ -59,34 +59,32 @@ clientaddr {{ trap_source }}
# SNMPS template - be careful if you edit the template.
access_config_tmpl = """
### Autogenerated by snmp.py ###
-{% if v3_users %}
-{% for u in v3_users %}
+{%- for u in v3_users %}
{{ u.mode }}user {{ u.name }}
-{% endfor %}
-{% endif -%}
+{%- endfor %}
+
rwuser {{ vyos_user }}
+
"""
# SNMPS template - 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 %}
+{%- if v3_engineid %}
oldEngineID {{ v3_engineid }}
-{%- endif -%}
+{%- endif %}
"""
# SNMPS template - be careful if you edit the template.
@@ -123,112 +121,108 @@ monitor -r 10 -e linkDownTrap "Generate linkDown" ifOperStatus == 2
########################
# configurable section #
########################
-
{% if v3_tsm_key %}
[snmp] localCert {{ v3_tsm_key }}
-{% endif %}
+{%- endif %}
# Default system description is VyOS version
sysDescr VyOS {{ version }}
-{% if description -%}
+{% if description %}
# Description
SysDescr {{ description }}
-{% endif %}
+{%- endif %}
# Listen
agentaddress unix:/run/snmpd.socket{% if listen_on %}{% for li in listen_on %},{{ li }}{% endfor %}{% else %},udp:161,udp6:161{% endif %}{% if v3_tsm_key %},tlstcp:{{ v3_tsm_port }},dtlsudp::{{ v3_tsm_port }}{% endif %}
# SNMP communities
-{% if communities -%}
-{% for c in communities %}
-{% if c.network -%}
-{% for network in c.network_v4 %}
+{%- for c in communities %}
+{%- if c.network_v4 %}
+{%- for network in c.network_v4 %}
{{ c.authorization }}community {{ c.name }} {{ network }}
-{% endfor %}
-{% for network in c.network_v6 %}
-{{ c.authorization }}community6 {{ c.name }} {{ network }}
-{% endfor %}
-{% else %}
+{%- endfor %}
+{%- else %}
{{ c.authorization }}community {{ c.name }}
+{%- endif %}
+{%- if c.network_v6 %}
+{%- for network in c.network_v6 %}
+{{ c.authorization }}community6 {{ c.name }} {{ network }}
+{%- endfor %}
+{%- else %}
{{ c.authorization }}community6 {{ c.name }}
-{% endif %}
-{% endfor %}
-{% endif %}
+{%- endif %}
+{%- endfor %}
-{% if contact -%}
+{% if contact %}
# system contact information
SysContact {{ contact }}
-{% endif %}
+{%- endif %}
-{% if location -%}
+{% if location %}
# system location information
SysLocation {{ location }}
-{% endif %}
+{%- endif %}
{% if smux_peers -%}
# additional smux peers
-{% for sp in smux_peers %}
+{%- for sp in smux_peers %}
smuxpeer {{ sp }}
-{% endfor %}
-{% endif %}
+{%- endfor %}
+{%- endif %}
{% if trap_targets -%}
# if there is a problem - tell someone!
-{% for t in trap_targets %}
+{%- for t in trap_targets %}
trap2sink {{ t.target }}{% if t.port -%}:{{ t.port }}{% endif %} {{ t.community }}
-{% endfor %}
-{% endif %}
+{%- endfor %}
+{%- endif %}
+{%- if v3_enabled %}
#
# SNMPv3 stuff goes here
#
-{% if v3_enabled %}
-
# views
-{% if v3_views -%}
-{% for v in v3_views %}
-{% for oid in v.oids %}
+{%- for v in v3_views %}
+{%- for oid in v.oids %}
view {{ v.name }} included .{{ oid.oid }}
-{% endfor %}
-{% endfor %}
-{% endif %}
+{%- endfor %}
+{%- endfor %}
# access
# context sec.model sec.level match read write notif
-{% if v3_groups -%}
-{% for g in v3_groups %}
-{% if g.mode == 'ro' %}
-access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} none none
-access {{ g.name }} "" tsm {{ g.seclevel }} exact {{ g.view }} none none
-{% elif g.mode == 'rw' %}
-access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} {{ g.view }} none
-access {{ g.name }} "" tsm {{ g.seclevel }} exact {{ g.view }} {{ g.view }} none
-{% endif %}
-{% endfor -%}
-{% endif %}
+{%- for g in v3_groups %}
+access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} {% if g.mode == 'ro' %}none{% else %}{{ g.view }}{% endif %} none
+access {{ g.name }} "" tsm {{ g.seclevel }} exact {{ g.view }} {% if g.mode == 'ro' %}none{% else %}{{ g.view }}{% endif %} none
+{%- endfor %}
# trap-target
-{% if v3_traps -%}
-{% for t in v3_traps %}
+{%- for t in v3_traps %}
trapsess -v 3 {{ '-Ci' if t.type == 'inform' }} -e {{ t.engineID }} -u {{ t.secName }} -l {{ t.secLevel }} -a {{ t.authProtocol }} {% if t.authPassword %}-A {{ t.authPassword }}{% elif t.authMasterKey %}-3m {{ t.authMasterKey }}{% endif %} -x {{ t.privProtocol }} {% if t.privPassword %}-X {{ t.privPassword }}{% elif t.privMasterKey %}-3M {{ t.privMasterKey }}{% endif %} {{ t.ipProto }}:{{ t.ipAddr }}:{{ t.ipPort }}
-{% endfor -%}
-{% endif %}
+{%- endfor %}
# group
-{% if v3_users -%}
-{% for u in v3_users %}
+{%- for u in v3_users %}
group {{ u.group }} usm {{ u.name }}
group {{ u.group }} tsm {{ u.name }}
{% endfor %}
-{% endif %}
+{%- endif %}
+"""
-{% endif %}
+init_config_tmpl = """
+### Autogenerated by snmp.py ###
+# This file controls the activity of snmpd
+# snmpd control (yes means start daemon).
+SNMPDRUN=yes
+
+# snmpd options (use syslog, close stdin/out/err).
+SNMPDOPTS='-LSed -u snmp -g snmp -p /run/snmpd.pid'
"""
default_config_data = {
'listen_on': [],
+ 'listen_address': [],
'communities': [],
'smux_peers': [],
'location' : '',
@@ -281,12 +275,21 @@ def get_config():
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)):
for addr in conf.return_values('community {0} network'.format(name)):
if vyos.validate.is_ipv4(addr):
- community['network_v4'] = addr
+ community['network_v4'].append(addr)
else:
- community['network_v6'] = addr
+ 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)
@@ -298,19 +301,20 @@ def get_config():
if conf.exists('listen-address'):
for addr in conf.list_nodes('listen-address'):
- listen = ''
port = '161'
if conf.exists('listen-address {0} port'.format(addr)):
port = conf.return_value('listen-address {0} port'.format(addr))
- if vyos.validate.is_ipv4(addr):
- # udp:127.0.0.1:161
- listen = 'udp:' + addr + ':' + port
- else:
- # udp6:[::1]:161
- listen = 'udp6:' + '[' + addr + ']' + ':' + port
+ 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')
@@ -585,6 +589,24 @@ def verify(snmp):
if not os.path.isfile('/config/snmp/tls/certs/' + snmp['v3_tsm_key']):
raise ConfigError('TSM key must be fingerprint or filename in "/config/snmp/tls/certs/" folder')
+ for listen in snmp['listen_address']:
+ addr = listen[0]
+ port = listen[1]
+
+ if vyos.validate.is_ipv4(addr):
+ # example: udp:127.0.0.1:161
+ listen = 'udp:' + addr + ':' + port
+ else:
+ # example: udp6:[::1]:161
+ listen = 'udp6:' + '[' + addr + ']' + ':' + port
+
+ # We only wan't to configure addresses that exist on the system.
+ # Hint the user if they don't exist
+ if vyos.validate.is_addr_assigned(addr):
+ snmp['listen_on'].append(listen)
+ else:
+ print('WARNING: SNMP listen address {0} not configured!'.format(addr))
+
if 'v3_groups' in snmp.keys():
for group in snmp['v3_groups']:
#
@@ -711,29 +733,35 @@ def generate(snmp):
return None
# Write client config file
- tmpl = jinja2.Template(client_config_tmpl, trim_blocks=True)
+ tmpl = jinja2.Template(client_config_tmpl)
config_text = tmpl.render(snmp)
with open(config_file_client, 'w') as f:
f.write(config_text)
# Write server config file
- tmpl = jinja2.Template(daemon_config_tmpl, trim_blocks=True)
+ tmpl = jinja2.Template(daemon_config_tmpl)
config_text = tmpl.render(snmp)
with open(config_file_daemon, 'w') as f:
f.write(config_text)
# Write access rights config file
- tmpl = jinja2.Template(access_config_tmpl, trim_blocks=True)
+ tmpl = jinja2.Template(access_config_tmpl)
config_text = tmpl.render(snmp)
with open(config_file_access, 'w') as f:
f.write(config_text)
# Write access rights config file
- tmpl = jinja2.Template(user_config_tmpl, trim_blocks=True)
+ tmpl = jinja2.Template(user_config_tmpl)
config_text = tmpl.render(snmp)
with open(config_file_user, 'w') as f:
f.write(config_text)
+ # Write init config file
+ tmpl = jinja2.Template(init_config_tmpl)
+ config_text = tmpl.render(snmp)
+ with open(config_file_init, 'w') as f:
+ f.write(config_text)
+
return None
def apply(snmp):
@@ -767,9 +795,17 @@ def apply(snmp):
# start SNMP daemon
os.system("sudo systemctl restart snmpd.service")
- # the passwords are not available immediately so this is a workaround
- # and should be changed to polling
- time.sleep(2)
+ # Passwords are not available immediately in the configuration file,
+ # after daemon startup - we wait until they have been processed by
+ # snmpd, which we see when a magic line appears in this file.
+ snmpReady = False
+ while not snmpReady:
+ with open(config_file_user, 'r') as f:
+ for line in f:
+ # Search for our magic string inside the file
+ if '**** DO NOT EDIT THIS FILE ****' in line:
+ snmpReady = True
+ break
# Back in the Perl days the configuration was re-read and any
# plaintext password inside the configuration was replaced by
diff --git a/src/conf_mode/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
index b6cf5c09e..0984b4545 100755
--- a/src/conf_mode/tftp_server.py
+++ b/src/conf_mode/tftp_server.py
@@ -96,12 +96,20 @@ def verify(tftpd):
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):
- raise ConfigError('TFTP server IPv4 listen address "{0}" not configured!'.format(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):
- raise ConfigError('TFTP server IPv6 listen address "{0}" not configured!'.format(addr))
+ print('WARNING: TFTP server listen address {0} not configured!'.format(addr))
return None
diff --git a/src/conf_mode/wireguard.py b/src/conf_mode/wireguard.py
index 9848914e3..c6440ad81 100755
--- a/src/conf_mode/wireguard.py
+++ b/src/conf_mode/wireguard.py
@@ -28,6 +28,7 @@ from vyos import ConfigError
dir = r'/config/auth/wireguard'
pk = dir + '/private.key'
pub = dir + '/public.key'
+psk_file = r'/tmp/psk'
def check_kmod():
if not os.path.exists('/sys/module/wireguard'):
@@ -117,7 +118,9 @@ def get_config():
config_data['interfaces'][intfc]['peer'][p]['endpoint'] = c.return_value(cnf + ' peer ' + p + ' endpoint')
if c.exists(cnf + ' peer ' + p + ' persistent-keepalive'):
config_data['interfaces'][intfc]['peer'][p]['persistent-keepalive'] = c.return_value(cnf + ' peer ' + p + ' persistent-keepalive')
-
+ if c.exists(cnf + ' peer ' + p + ' preshared-key'):
+ config_data['interfaces'][intfc]['peer'][p]['psk'] = c.return_value(cnf + ' peer ' + p + ' preshared-key')
+
return config_data
def verify(c):
@@ -225,24 +228,22 @@ def apply(c):
fh.write(str(cnf_descr))
def configure_interface(c, intf):
- wg_config = {
+ for p in c['interfaces'][intf]['peer']:
+ ## config init for wg call
+ wg_config = {
'interface' : intf,
- 'port' : 0,
- 'private-key' : '/config/auth/wireguard/private.key',
- 'peer' :
- {
- 'pubkey' : ''
- },
+ 'port' : 0,
+ 'private-key' : pk,
+ 'pubkey' : '',
+ 'psk' : '/dev/null',
'allowed-ips' : [],
'fwmark' : 0x00,
'endpoint' : None,
'keepalive' : 0
-
}
- for p in c['interfaces'][intf]['peer']:
## mandatory settings
- wg_config['peer']['pubkey'] = c['interfaces'][intf]['peer'][p]['pubkey']
+ wg_config['pubkey'] = c['interfaces'][intf]['peer'][p]['pubkey']
wg_config['allowed-ips'] = c['interfaces'][intf]['peer'][p]['allowed-ips']
## optional settings
@@ -258,11 +259,19 @@ def configure_interface(c, intf):
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['peer']['pubkey']
+ 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]:
@@ -279,7 +288,11 @@ def configure_interface(c, intf):
cmd += " persistent-keepalive 0"
sl.syslog(sl.LOG_NOTICE, cmd)
+ #print (cmd)
subprocess.call([cmd], shell=True)
+ """ remove psk_file """
+ if os.path.exists(psk_file):
+ os.remove(psk_file)
def add_addr(intf, addr):
ret = subprocess.call(['ip a a dev ' + intf + ' ' + addr + ' &>/dev/null'], shell=True)