summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--debian/control1
-rw-r--r--interface-definitions/tftp-server.xml57
-rwxr-xr-xsrc/conf_mode/tftp_server.py159
3 files changed, 217 insertions, 0 deletions
diff --git a/debian/control b/debian/control
index 70bf7a61c..04b228737 100644
--- a/debian/control
+++ b/debian/control
@@ -38,6 +38,7 @@ Depends: python3,
beep,
keepalived (>=2.0.5),
wireguard,
+ tftpd-hpa,
${shlibs:Depends},
${misc:Depends}
Description: VyOS configuration scripts and data
diff --git a/interface-definitions/tftp-server.xml b/interface-definitions/tftp-server.xml
new file mode 100644
index 000000000..2874b034c
--- /dev/null
+++ b/interface-definitions/tftp-server.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0"?>
+<!-- TFTP configuration -->
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="tftp-server" owner="${vyos_conf_scripts_dir}/tftp_server.py">
+ <properties>
+ <help>Trivial File Transfer Protocol (TFTP) server</help>
+ <priority>990</priority>
+ </properties>
+ <children>
+ <leafNode name="directory">
+ <properties>
+ <help>Folder containing files served by TFTP [REQUIRED]</help>
+ </properties>
+ </leafNode>
+ <leafNode name="allow-upload">
+ <properties>
+ <help>Allow TFTP file uploads</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="port">
+ <properties>
+ <help>Port for TFTP service</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port (default: 69)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="listen-address">
+ <properties>
+ <help>Addresses for TFTP server to listen [REQUIRED]</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>TFTP IPv4 listen address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>TFTP IPv6 listen address</description>
+ </valueHelp>
+ <multi/>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py
new file mode 100755
index 000000000..8133ed215
--- /dev/null
+++ b/src/conf_mode/tftp_server.py
@@ -0,0 +1,159 @@
+#!/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 ipaddress
+import netifaces
+
+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 002{% endif %}"
+"""
+
+default_config_data = {
+ 'directory': '',
+ 'allow_upload': False,
+ 'port': '69',
+ 'listen_ipv4': [],
+ 'listen_ipv6': []
+}
+
+# Verify if an IP address is assigned to any interface, IPv4 and IPv6
+def addrok(ipaddr, ipversion):
+ for interface in netifaces.interfaces():
+ # Retrieve IP address of network interfaces
+ if ipversion in netifaces.ifaddresses(interface).keys():
+ for addr in netifaces.ifaddresses(interface)[ipversion]:
+ if addr['addr'] == ipaddr:
+ return True
+
+ return False
+
+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 (ipaddress.ip_address(addr).version == 4):
+ tftpd['listen_ipv4'].append(addr)
+
+ if (ipaddress.ip_address(addr).version == 6):
+ 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 address in tftpd['listen_ipv4']:
+ if not addrok(address, netifaces.AF_INET):
+ raise ConfigError('TFTP server listen address "{0}" not configured on this system.'.format(address))
+
+ for address in tftpd['listen_ipv6']:
+ if not addrok(address, netifaces.AF_INET6):
+ raise ConfigError('TFTP server listen address "{0}" not configured on this system.'.format(address))
+
+ 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 uid for user 'snmp'
+ 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)