diff options
Diffstat (limited to 'src')
| -rwxr-xr-x | src/services/vyos-hostsd | 254 | ||||
| -rw-r--r-- | src/systemd/vyos-hostsd.service | 22 | ||||
| -rwxr-xr-x | src/utils/vyos-hostsd-client | 82 | 
3 files changed, 358 insertions, 0 deletions
| diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd new file mode 100755 index 000000000..972c2e3dc --- /dev/null +++ b/src/services/vyos-hostsd @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. +# +# + +import os +import sys +import time +import json +import traceback + +import zmq + +import jinja2 + +debug = True + +DATA_DIR = "/var/lib/vyos/" +STATE_FILE = os.path.join(DATA_DIR, "hostsd.state") + +SOCKET_PATH = "ipc:///run/vyos-hostsd.sock" + +RESOLV_CONF_FILE = '/etc/resolv.conf' +HOSTS_FILE = '/etc/hosts' + +hosts_tmpl_source = """ +### Autogenerated by VyOS ### +127.0.0.1       localhost +127.0.1.1       {{ host_name }}{% if domain_name %}.{{ domain_name }}{% endif %} + +# The following lines are desirable for IPv6 capable hosts +::1             localhost ip6-localhost ip6-loopback +fe00::0         ip6-localnet +ff00::0         ip6-mcastprefix +ff02::1         ip6-allnodes +ff02::2         ip6-allrouters + + +{%- if hosts %} +{% for h in hosts -%} +{{hosts[h]['address']}}\t{{h}}\t{% for a in hosts[h]['aliases'] %} {{a}} {% endfor %} +{% endfor %} +{%- endif %} +""" + +hosts_tmpl = jinja2.Template(hosts_tmpl_source) + +resolv_tmpl_source = """ +### Autogenerated by VyOS ### +{% for ns in name_servers -%} +nameserver {{ns}} +{% endfor -%} + +{%- if domain_name %} +domain {{ domain_name }} +{%- endif %} + +{%- if search_domains %} +search {{ search_domains | join(" ") }} +{%- endif %} + +""" + +resolv_tmpl = jinja2.Template(resolv_tmpl_source) + +# The state data includes a list of name servers +# and a list of hosts entries. +# +# Name servers have the following structure: +# {"server": {"tag": <str>}} +# +# Hosts entries are similar: +# {"host": {"tag": <str>, "address": <str>}} +# +# The tag is either "static" or "dhcp-<intf>" +# It's used to distinguish entries created +# by different scripts so that they can be removed +# and re-created without having to track what needs +# to be changed +STATE = { +    "name_servers": {}, +    "hosts": {}, +    "host_name": "vyos", +    "domain_name": "", +    "search_domains": []} + + +def make_resolv_conf(data): +    resolv_conf = resolv_tmpl.render(data) +    print("Writing /etc/resolv.conf") +    with open(RESOLV_CONF_FILE, 'w') as f: +        f.write(resolv_conf) + +def make_hosts_file(state): +    print("Writing /etc/hosts") +    hosts = hosts_tmpl.render(state) +    with open(HOSTS_FILE, 'w') as f: +        f.write(hosts) + +def add_hosts(data, entries, tag): +    hosts = data['hosts'] + +    if not entries: +        return + +    for e in entries: +        host = e['host'] +        hosts[host] = {} +        hosts[host]['tag'] = tag +        hosts[host]['address'] = e['address'] +        hosts[host]['aliases'] = e['aliases'] + +def delete_hosts(data, tag): +    hosts = data['hosts'] +    keys_for_deletion = [] + +    # You can't delete items from a dict while iterating over it, +    # so we build a list of doomed items first +    for h in hosts: +        if hosts[h]['tag'] == tag: +            keys_for_deletion.append(h) + +    for k in keys_for_deletion: +        del hosts[k] + +def add_name_servers(data, entries, tag): +    name_servers = data['name_servers'] + +    if not entries: +        return + +    for e in entries: +        name_servers[e] = {} +        name_servers[e]['tag'] = tag + +def delete_name_servers(data, tag): +    name_servers = data['name_servers'] +    keys_for_deletion = [] + +    for ns in name_servers: +      if name_servers[ns]['tag'] == tag: +          keys_for_deletion.append(ns) + +    for k in keys_for_deletion: +      del name_servers[k] + +def set_host_name(state, data): +    if data['host_name']: +        state['host_name'] = data['host_name'] +    if data['domain_name']: +        state['domain_name'] = data['domain_name'] +    if data['search_domains']: +        state['search_domains'] = data['search_domains'] + +def get_option(msg, key): +    if key in msg: +        return msg[key] +    else: +        raise ValueError("Missing required option \"{0}\"".format(key)) + +def handle_message(msg_json): +    msg = json.loads(msg_json) + +    op = get_option(msg, 'op') +    _type = get_option(msg, 'type') + +    if op == 'delete': +        tag = get_option(msg, 'tag') + +        if _type == 'name_servers': +            delete_name_servers(STATE, tag) +        elif _type == 'hosts': +            delete_hosts(STATE, tag) +        else: +            raise ValueError("Unknown message type {0}".format(_type)) +    elif op == 'add': +        tag = get_option(msg, 'tag') +        entries = get_option(msg, 'data') +        if _type == 'name_servers': +            add_name_servers(STATE, entries, tag) +        elif _type == 'hosts': +            add_hosts(STATE, entries, tag) +        else: +            raise ValueError("Unknown message type {0}".format(_type)) +    elif op == 'set': +        # Host name/domain name/search domain are set without a tag, +        # there can be only one anyway +        data = get_option(msg, 'data') +        if _type == 'host_name': +            set_host_name(STATE, data) +        else: +            raise ValueError("Unknown message type {0}".format(_type)) +    else: +        raise ValueError("Unknown operation {0}".format(op)) + +    make_resolv_conf(STATE) +    make_hosts_file(STATE) + +    print("Saving state to {0}".format(STATE_FILE)) +    with open(STATE_FILE, 'w') as f: +        json.dump(STATE, f) + + +if __name__ == '__main__': +    # Create a directory for state checkpoints +    os.makedirs(DATA_DIR, exist_ok=True) +    if os.path.exists(STATE_FILE): +        with open(STATE_FILE, 'r') as f: +            try: +                data = json.load(f) +                STATE = data +            except: +                print(traceback.format_exc()) +                print("Failed to load the state file, using default") + +    context = zmq.Context() +    socket = context.socket(zmq.REP) +    socket.bind(SOCKET_PATH) + +    while True: +        #  Wait for next request from client +        message = socket.recv().decode() +        print("Received a configuration change request") +        if debug: +            print("Request data: {0}".format(message)) + +        resp = {} + +        try: +            handle_message(message) +        except ValueError as e: +            resp['error'] = str(e) +        except: +            print(traceback.format_exc()) +            resp['error'] = "Internal error" + +        if debug: +            print("Sent response: {0}".format(resp)) + +        #  Send reply back to client +        socket.send(json.dumps(resp).encode()) diff --git a/src/systemd/vyos-hostsd.service b/src/systemd/vyos-hostsd.service new file mode 100644 index 000000000..fe6c163d7 --- /dev/null +++ b/src/systemd/vyos-hostsd.service @@ -0,0 +1,22 @@ +[Unit] +Description=VyOS DNS configuration keeper +After=auditd.service systemd-user-sessions.service time-sync.target + +[Service] +ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-hostsd +Type=idle +KillMode=process + +SyslogIdentifier=vyos-hostsd +SyslogFacility=daemon + +Restart=on-failure + +# Does't work but leave it here +User=root +Group=vyattacfg + +[Install] +# Installing in a earlier target leaves ExecStartPre waiting +WantedBy=getty.target + diff --git a/src/utils/vyos-hostsd-client b/src/utils/vyos-hostsd-client new file mode 100755 index 000000000..d3105c9cf --- /dev/null +++ b/src/utils/vyos-hostsd-client @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import argparse + +import vyos.hostsd_client + +parser = argparse.ArgumentParser() +group = parser.add_mutually_exclusive_group() +group.add_argument('--add-hosts', action="store_true") +group.add_argument('--delete-hosts', action="store_true") +group.add_argument('--add-name-servers', action="store_true") +group.add_argument('--delete-name-servers', action="store_true") +group.add_argument('--set-host-name', action="store_true") + +parser.add_argument('--host', type=str, action="append") +parser.add_argument('--name-server', type=str, action="append") +parser.add_argument('--host-name', type=str) +parser.add_argument('--domain-name', type=str) +parser.add_argument('--search-domain', type=str, action="append") + +parser.add_argument('--tag', type=str) + +args = parser.parse_args() + +try: +    client = vyos.hostsd_client.Client() + +    if args.add_hosts: +        if not args.tag: +            raise ValueError("Tag is required for this operation") +        data = [] +        for h in args.host: +            entry = {} +            params = h.split(",") +            if len(params) < 2: +                raise ValueError("Malformed host entry") +            entry['host'] = params[0] +            entry['address'] = params[1] +            entry['aliases'] = params[2:] +            data.append(entry) +        client.add_hosts(args.tag, data) +    elif args.delete_hosts: +        if not args.tag: +            raise ValueError("Tag is required for this operation") +        client.delete_hosts(args.tag) +    elif args.add_name_servers: +        if not args.tag: +            raise ValueError("Tag is required for this operation") +        client.add_name_servers(args.tag, args.name_server) +    elif args.delete_name_servers: +        if not args.tag: +            raise ValueError("Tag is required for this operation") +        client.delete_name_servers(args.tag) +    elif args.set_host_name: +        client.set_host_name(args.host_name, args.domain_name, args.search_domain) +    else: +        raise ValueError("Operation required") + +except ValueError as e: +    print("Incorrect options: {0}".format(e)) +    sys.exit(1) +except vyos.hostsd_client.VyOSHostsdError as e: +    print("Server returned an error: {0}".format(e)) +    sys.exit(1) + | 
