Page Menu
Home
VyOS Platform
Search
Configure Global Search
Log In
Files
F71813255
vyos-hostsd
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
17 KB
Referenced Files
None
Subscribers
None
vyos-hostsd
View Options
#!/usr/bin/env python3
#
# Copyright (C) 2019-2020 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/>.
#
#########
# USAGE #
#########
# This daemon listens on its socket for JSON messages.
# The received message format is:
#
# { 'type': '<message type>',
# 'op': '<message operation>',
# 'data': <data list or dict>
# }
#
# For supported message types, see below.
# 'op' can be 'add', delete', 'get', 'set' or 'apply'.
# Different message types support different sets of operations and different
# data formats.
#
# Changes to configuration made via add or delete don't take effect immediately,
# they are remembered in a state variable and saved to disk to a state file.
# State is remembered across daemon restarts but not across system reboots
# as it's saved in a temporary filesystem (/run).
#
# 'apply' is a special operation that applies the configuration from the cached
# state, rendering all config files and reloading relevant daemons (currently
# just pdns-recursor via rec-control).
#
# note: 'add' operation also acts as 'update' as it uses dict.update, if the
# 'data' dict item value is a dict. If it is a list, it uses list.append.
#
### tags
# Tags can be arbitrary, but they are generally in this format:
# 'static', 'system', 'dhcp(v6)-<intf>' or 'dhcp-server-<client ip>'
# They are used to distinguish entries created by different scripts so they can
# be removed and recreated without having to track what needs to be changed.
# They are also used as a way to control which tags settings (e.g. nameservers)
# get added to various config files via name_server_tags_(recursor|system)
#
### name_server_tags_(recursor|system)
# A list of tags whose nameservers and search domains is used to generate
# /etc/resolv.conf and pdns-recursor config.
# system list is used to generate resolv.conf.
# recursor list is used to generate pdns-rec forward-zones.
# When generating each file, the order of nameservers is as per the order of
# name_server_tags (the order in which tags were added), then the order in
# which the name servers for each tag were added.
#
#### Message types
#
### name_servers
#
# { 'type': 'name_servers',
# 'op': 'add',
# 'data': {
# '<str tag>': ['<str nameserver>', ...],
# ...
# }
# }
#
# { 'type': 'name_servers',
# 'op': 'delete',
# 'data': ['<str tag>', ...]
# }
#
# { 'type': 'name_servers',
# 'op': 'get',
# 'tag_regex': '<str regex>'
# }
# response:
# { 'data': {
# '<str tag>': ['<str nameserver>', ...],
# ...
# }
# }
#
### name_server_tags
#
# { 'type': 'name_server_tags',
# 'op': 'add',
# 'data': ['<str tag>', ...]
# }
#
# { 'type': 'name_server_tags',
# 'op': 'delete',
# 'data': ['<str tag>', ...]
# }
#
# { 'type': 'name_server_tags',
# 'op': 'get',
# }
# response:
# { 'data': ['<str tag>', ...] }
#
### forward_zones
## Additional zones added to pdns-recursor forward-zones-file.
## If recursion_desired is true, '+' will be prepended to the zone line.
## If addnta is true, a NTA (Negative Trust Anchor) will be added via
## lua-config-file.
#
# { 'type': 'forward_zones',
# 'op': 'add',
# 'data': {
# '<str zone>': {
# 'server': ['<str nameserver>', ...],
# 'addnta': <bool>,
# 'recursion_desired': <bool>
# }
# ...
# }
# }
#
# { 'type': 'forward_zones',
# 'op': 'delete',
# 'data': ['<str zone>', ...]
# }
#
# { 'type': 'forward_zones',
# 'op': 'get',
# }
# response:
# { 'data': {
# '<str zone>': { ... },
# ...
# }
# }
#
#
### authoritative_zones
## Additional zones hosted authoritatively by pdns-recursor.
## We add NTAs for these zones but do not do much else here.
#
# { 'type': 'authoritative_zones',
# 'op': 'add',
# 'data': ['<str zone>', ...]
# }
#
# { 'type': 'authoritative_zones',
# 'op': 'delete',
# 'data': ['<str zone>', ...]
# }
#
# { 'type': 'authoritative_zones',
# 'op': 'get',
# }
# response:
# { 'data': ['<str zone>', ...] }
#
#
### search_domains
#
# { 'type': 'search_domains',
# 'op': 'add',
# 'data': {
# '<str tag>': ['<str domain>', ...],
# ...
# }
# }
#
# { 'type': 'search_domains',
# 'op': 'delete',
# 'data': ['<str tag>', ...]
# }
#
# { 'type': 'search_domains',
# 'op': 'get',
# }
# response:
# { 'data': {
# '<str tag>': ['<str domain>', ...],
# ...
# }
# }
#
### hosts
#
# { 'type': 'hosts',
# 'op': 'add',
# 'data': {
# '<str tag>': {
# '<str host>': {
# 'address': '<str address>',
# 'aliases': ['<str alias>, ...]
# },
# ...
# },
# ...
# }
# }
#
# { 'type': 'hosts',
# 'op': 'delete',
# 'data': ['<str tag>', ...]
# }
#
# { 'type': 'hosts',
# 'op': 'get'
# 'tag_regex': '<str regex>'
# }
# response:
# { 'data': {
# '<str tag>': {
# '<str host>': {
# 'address': '<str address>',
# 'aliases': ['<str alias>, ...]
# },
# ...
# },
# ...
# }
# }
### host_name
#
# { 'type': 'host_name',
# 'op': 'set',
# 'data': {
# 'host_name': '<str hostname>'
# 'domain_name': '<str domainname>'
# }
# }
import
os
import
sys
import
time
import
json
import
signal
import
traceback
import
re
import
logging
import
zmq
from
voluptuous
import
Schema
,
MultipleInvalid
,
Required
,
Any
from
collections
import
OrderedDict
from
vyos.util
import
popen
,
chown
,
chmod_755
,
makedir
,
process_named_running
from
vyos.template
import
render
debug
=
True
# Configure logging
logger
=
logging
.
getLogger
(
__name__
)
# set stream as output
logs_handler
=
logging
.
StreamHandler
()
logger
.
addHandler
(
logs_handler
)
if
debug
:
logger
.
setLevel
(
logging
.
DEBUG
)
else
:
logger
.
setLevel
(
logging
.
INFO
)
RUN_DIR
=
"/run/vyos-hostsd"
STATE_FILE
=
os
.
path
.
join
(
RUN_DIR
,
"vyos-hostsd.state"
)
SOCKET_PATH
=
"ipc://"
+
os
.
path
.
join
(
RUN_DIR
,
'vyos-hostsd.sock'
)
RESOLV_CONF_FILE
=
'/etc/resolv.conf'
HOSTS_FILE
=
'/etc/hosts'
PDNS_REC_USER
=
PDNS_REC_GROUP
=
'pdns'
PDNS_REC_RUN_DIR
=
'/run/powerdns'
PDNS_REC_LUA_CONF_FILE
=
f
'
{
PDNS_REC_RUN_DIR
}
/recursor.vyos-hostsd.conf.lua'
PDNS_REC_ZONES_FILE
=
f
'
{
PDNS_REC_RUN_DIR
}
/recursor.forward-zones.conf'
STATE
=
{
"name_servers"
:
{},
"name_server_tags_recursor"
:
[],
"name_server_tags_system"
:
[],
"forward_zones"
:
{},
"authoritative_zones"
:
[],
"hosts"
:
{},
"host_name"
:
"vyos"
,
"domain_name"
:
""
,
"search_domains"
:
{},
"changes"
:
0
}
# the base schema that every received message must be in
base_schema
=
Schema
({
Required
(
'op'
):
Any
(
'add'
,
'delete'
,
'set'
,
'get'
,
'apply'
),
'type'
:
Any
(
'name_servers'
,
'name_server_tags_recursor'
,
'name_server_tags_system'
,
'forward_zones'
,
'authoritative_zones'
,
'search_domains'
,
'hosts'
,
'host_name'
),
'data'
:
Any
(
list
,
dict
),
'tag'
:
str
,
'tag_regex'
:
str
})
# more specific schemas
op_schema
=
Schema
({
'op'
:
str
,
},
required
=
True
)
op_type_schema
=
op_schema
.
extend
({
'type'
:
str
,
},
required
=
True
)
host_name_add_schema
=
op_type_schema
.
extend
({
'data'
:
{
'host_name'
:
str
,
'domain_name'
:
Any
(
str
,
None
)
}
},
required
=
True
)
data_dict_list_schema
=
op_type_schema
.
extend
({
'data'
:
{
str
:
[
str
]
}
},
required
=
True
)
data_list_schema
=
op_type_schema
.
extend
({
'data'
:
[
str
]
},
required
=
True
)
tag_regex_schema
=
op_type_schema
.
extend
({
'tag_regex'
:
str
},
required
=
True
)
forward_zone_add_schema
=
op_type_schema
.
extend
({
'data'
:
{
str
:
{
'server'
:
[
str
],
'addnta'
:
Any
({},
None
),
'recursion_desired'
:
Any
({},
None
),
}
}
},
required
=
False
)
hosts_add_schema
=
op_type_schema
.
extend
({
'data'
:
{
str
:
{
str
:
{
'address'
:
[
str
],
'aliases'
:
[
str
]
}
}
}
},
required
=
True
)
# op and type to schema mapping
msg_schema_map
=
{
'name_servers'
:
{
'add'
:
data_dict_list_schema
,
'delete'
:
data_list_schema
,
'get'
:
tag_regex_schema
},
'name_server_tags_recursor'
:
{
'add'
:
data_list_schema
,
'delete'
:
data_list_schema
,
'get'
:
op_type_schema
},
'name_server_tags_system'
:
{
'add'
:
data_list_schema
,
'delete'
:
data_list_schema
,
'get'
:
op_type_schema
},
'forward_zones'
:
{
'add'
:
forward_zone_add_schema
,
'delete'
:
data_list_schema
,
'get'
:
op_type_schema
},
'authoritative_zones'
:
{
'add'
:
data_list_schema
,
'delete'
:
data_list_schema
,
'get'
:
op_type_schema
},
'search_domains'
:
{
'add'
:
data_dict_list_schema
,
'delete'
:
data_list_schema
,
'get'
:
tag_regex_schema
},
'hosts'
:
{
'add'
:
hosts_add_schema
,
'delete'
:
data_list_schema
,
'get'
:
tag_regex_schema
},
'host_name'
:
{
'set'
:
host_name_add_schema
},
None
:
{
'apply'
:
op_schema
}
}
def
validate_schema
(
data
):
base_schema
(
data
)
try
:
schema
=
msg_schema_map
[
data
[
'type'
]
if
'type'
in
data
else
None
][
data
[
'op'
]]
schema
(
data
)
except
KeyError
:
raise
ValueError
((
'Invalid or unknown combination: '
f
'op: "
{
data
[
"op"
]
}
", type: "
{
data
[
"type"
]
}
"'
))
def
pdns_rec_control
(
command
):
# pdns-r process name is NOT equal to the name shown in ps
if
not
process_named_running
(
'pdns-r/worker'
):
logger
.
info
(
f
'pdns_recursor not running, not sending "
{
command
}
"'
)
return
logger
.
info
(
f
'Running "rec_control
{
command
}
"'
)
(
ret
,
ret_code
)
=
popen
((
f
"rec_control --socket-dir=
{
PDNS_REC_RUN_DIR
}
{
command
}
"
))
if
ret_code
>
0
:
logger
.
exception
((
f
'"rec_control
{
command
}
" failed with exit status
{
ret_code
}
, '
f
'output: "
{
ret
}
"'
))
def
make_resolv_conf
(
state
):
logger
.
info
(
f
"Writing
{
RESOLV_CONF_FILE
}
"
)
render
(
RESOLV_CONF_FILE
,
'vyos-hostsd/resolv.conf.tmpl'
,
state
,
user
=
'root'
,
group
=
'root'
)
def
make_hosts
(
state
):
logger
.
info
(
f
"Writing
{
HOSTS_FILE
}
"
)
render
(
HOSTS_FILE
,
'vyos-hostsd/hosts.tmpl'
,
state
,
user
=
'root'
,
group
=
'root'
)
def
make_pdns_rec_conf
(
state
):
logger
.
info
(
f
"Writing
{
PDNS_REC_LUA_CONF_FILE
}
"
)
# on boot, /run/powerdns does not exist, so create it
makedir
(
PDNS_REC_RUN_DIR
,
user
=
PDNS_REC_USER
,
group
=
PDNS_REC_GROUP
)
chmod_755
(
PDNS_REC_RUN_DIR
)
render
(
PDNS_REC_LUA_CONF_FILE
,
'dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl'
,
state
,
user
=
PDNS_REC_USER
,
group
=
PDNS_REC_GROUP
)
logger
.
info
(
f
"Writing
{
PDNS_REC_ZONES_FILE
}
"
)
render
(
PDNS_REC_ZONES_FILE
,
'dns-forwarding/recursor.forward-zones.conf.tmpl'
,
state
,
user
=
PDNS_REC_USER
,
group
=
PDNS_REC_GROUP
)
def
set_host_name
(
state
,
data
):
if
data
[
'host_name'
]:
state
[
'host_name'
]
=
data
[
'host_name'
]
if
'domain_name'
in
data
:
state
[
'domain_name'
]
=
data
[
'domain_name'
]
def
add_items_to_dict
(
_dict
,
items
):
"""
Dedupes and preserves sort order.
"""
assert
isinstance
(
_dict
,
dict
)
assert
isinstance
(
items
,
dict
)
if
not
items
:
return
_dict
.
update
(
items
)
def
add_items_to_dict_as_keys
(
_dict
,
items
):
"""
Added item values are converted to OrderedDict with the value as keys
and null values. This is to emulate a list but with inherent deduplication.
Dedupes and preserves sort order.
"""
assert
isinstance
(
_dict
,
dict
)
assert
isinstance
(
items
,
dict
)
if
not
items
:
return
for
item
,
item_val
in
items
.
items
():
if
item
not
in
_dict
:
_dict
[
item
]
=
OrderedDict
({})
_dict
[
item
]
.
update
(
OrderedDict
.
fromkeys
(
item_val
))
def
add_items_to_list
(
_list
,
items
):
"""
Dedupes and preserves sort order.
"""
assert
isinstance
(
_list
,
list
)
assert
isinstance
(
items
,
list
)
if
not
items
:
return
for
item
in
items
:
if
item
not
in
_list
:
_list
.
append
(
item
)
def
delete_items_from_dict
(
_dict
,
items
):
"""
items is a list of keys to delete.
Doesn't error if the key doesn't exist.
"""
assert
isinstance
(
_dict
,
dict
)
assert
isinstance
(
items
,
list
)
for
item
in
items
:
if
item
in
_dict
:
del
_dict
[
item
]
def
delete_items_from_list
(
_list
,
items
):
"""
items is a list of items to remove.
Doesn't error if the key doesn't exist.
"""
assert
isinstance
(
_list
,
list
)
assert
isinstance
(
items
,
list
)
for
item
in
items
:
if
item
in
_list
:
_list
.
remove
(
item
)
def
get_items_from_dict_regex
(
_dict
,
item_regex_string
):
"""
Returns the items whose keys match item_regex_string.
"""
assert
isinstance
(
_dict
,
dict
)
assert
isinstance
(
item_regex_string
,
str
)
tmp
=
{}
regex
=
re
.
compile
(
item_regex_string
)
for
item
in
_dict
:
if
regex
.
match
(
item
):
tmp
[
item
]
=
_dict
[
item
]
return
tmp
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
):
result
=
None
op
=
get_option
(
msg
,
'op'
)
if
op
in
[
'add'
,
'delete'
,
'set'
]:
STATE
[
'changes'
]
+=
1
if
op
==
'delete'
:
_type
=
get_option
(
msg
,
'type'
)
data
=
get_option
(
msg
,
'data'
)
if
_type
in
[
'name_servers'
,
'forward_zones'
,
'search_domains'
,
'hosts'
]:
delete_items_from_dict
(
STATE
[
_type
],
data
)
elif
_type
in
[
'name_server_tags_recursor'
,
'name_server_tags_system'
,
'authoritative_zones'
]:
delete_items_from_list
(
STATE
[
_type
],
data
)
else
:
raise
ValueError
(
f
'Operation "
{
op
}
" unknown data type "
{
_type
}
"'
)
elif
op
==
'add'
:
_type
=
get_option
(
msg
,
'type'
)
data
=
get_option
(
msg
,
'data'
)
if
_type
in
[
'name_servers'
,
'search_domains'
]:
add_items_to_dict_as_keys
(
STATE
[
_type
],
data
)
elif
_type
in
[
'forward_zones'
,
'hosts'
]:
add_items_to_dict
(
STATE
[
_type
],
data
)
# maybe we need to rec_control clear-nta each domain that was removed here?
elif
_type
in
[
'name_server_tags_recursor'
,
'name_server_tags_system'
,
'authoritative_zones'
]:
add_items_to_list
(
STATE
[
_type
],
data
)
else
:
raise
ValueError
(
f
'Operation "
{
op
}
" unknown data type "
{
_type
}
"'
)
elif
op
==
'set'
:
_type
=
get_option
(
msg
,
'type'
)
data
=
get_option
(
msg
,
'data'
)
if
_type
==
'host_name'
:
set_host_name
(
STATE
,
data
)
else
:
raise
ValueError
(
f
'Operation "
{
op
}
" unknown data type "
{
_type
}
"'
)
elif
op
==
'get'
:
_type
=
get_option
(
msg
,
'type'
)
if
_type
in
[
'name_servers'
,
'search_domains'
,
'hosts'
]:
tag_regex
=
get_option
(
msg
,
'tag_regex'
)
result
=
get_items_from_dict_regex
(
STATE
[
_type
],
tag_regex
)
elif
_type
in
[
'name_server_tags_recursor'
,
'name_server_tags_system'
,
'forward_zones'
,
'authoritative_zones'
]:
result
=
STATE
[
_type
]
else
:
raise
ValueError
(
f
'Operation "
{
op
}
" unknown data type "
{
_type
}
"'
)
elif
op
==
'apply'
:
logger
.
info
(
f
"Applying
{
STATE
[
'changes'
]
}
changes"
)
make_resolv_conf
(
STATE
)
make_hosts
(
STATE
)
make_pdns_rec_conf
(
STATE
)
pdns_rec_control
(
'reload-lua-config'
)
pdns_rec_control
(
'reload-zones'
)
logger
.
info
(
"Success"
)
result
=
{
'message'
:
f
'Applied
{
STATE
[
"changes"
]
}
changes'
}
STATE
[
'changes'
]
=
0
else
:
raise
ValueError
(
f
"Unknown operation
{
op
}
"
)
logger
.
debug
(
f
"Saving state to
{
STATE_FILE
}
"
)
with
open
(
STATE_FILE
,
'w'
)
as
f
:
json
.
dump
(
STATE
,
f
)
return
result
if
__name__
==
'__main__'
:
# Create a directory for state checkpoints
os
.
makedirs
(
RUN_DIR
,
exist_ok
=
True
)
if
os
.
path
.
exists
(
STATE_FILE
):
with
open
(
STATE_FILE
,
'r'
)
as
f
:
try
:
STATE
=
json
.
load
(
f
)
except
:
logger
.
exception
(
traceback
.
format_exc
())
logger
.
exception
(
"Failed to load the state file, using default"
)
context
=
zmq
.
Context
()
socket
=
context
.
socket
(
zmq
.
REP
)
# Set the right permissions on the socket, then change it back
o_mask
=
os
.
umask
(
0o000
)
socket
.
bind
(
SOCKET_PATH
)
os
.
umask
(
o_mask
)
while
True
:
# Wait for next request from client
msg_json
=
socket
.
recv
()
.
decode
()
logger
.
debug
(
f
"Request data:
{
msg_json
}
"
)
try
:
msg
=
json
.
loads
(
msg_json
)
validate_schema
(
msg
)
resp
=
{}
resp
[
'data'
]
=
handle_message
(
msg
)
except
ValueError
as
e
:
resp
[
'error'
]
=
str
(
e
)
except
MultipleInvalid
as
e
:
# raised by schema
resp
[
'error'
]
=
f
'Invalid message:
{
str
(
e
)
}
'
logger
.
exception
(
resp
[
'error'
])
except
:
logger
.
exception
(
traceback
.
format_exc
())
resp
[
'error'
]
=
"Internal error"
# Send reply back to client
socket
.
send
(
json
.
dumps
(
resp
)
.
encode
())
logger
.
debug
(
f
"Sent response:
{
resp
}
"
)
File Metadata
Details
Attached
Mime Type
text/x-script.python
Expires
Fri, Jan 30, 11:45 AM (1 d, 13 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3284261
Default Alt Text
vyos-hostsd (17 KB)
Attached To
Mode
rVYOSONEX vyos-1x
Attached
Detach File
Event Timeline
Log In to Comment