#!/bin/bash # Copyright 2006-2009 Gentoo Foundation # Distributed under the terms of the GNU General Public License v2 # /etc/pdnsd/pdnsd.conf updater # Written by Oldrich Jedlicka (oldium.pro@seznam.cz) # Implementation notes: # * The assumptions are same as for other scripts like bind and dnsmasq: # - We assume that we are a local dns cache - after all, why would a server # use resolvconf? # - Now that we have assumed this, we also assume that generic DHCP clients # will enter their domains and search domains ONLY in the "search" field # in their resolv.confs and VPN clients will put the domain they are for # into the domain field only. # - This allows pdnsd to forward domains for a specific VPN domain to the # VPN nameserver and everything else to the standard name servers. # # HOW-TO CONFIGURE: # # To get this working, you need to do only two steps # # 1. Create a basic configuration of /etc/pdnsd/pdnsd.conf, you can use # /etc/pdnsd/pdnsd.conf.example to start. # # Additional configuration will be created automatically be resolvconf. # The generated server sections has labels starting with "resolvconf", so # # DO NOT USE "resolvconf" IN YOUR LABELS! # # Check if the status_ctl is set to "on", otherwise the configuration # will not be automatically reloaded - see sample config file. # # You are free to edit automatically created server sections, but always # write one option per line. There are few options that are always recreated # and your changes in them will be lost. Here is the list (with example # values): # # preset=on; # ip="192.168.0.1","192.168.0.2"; # include=".net",".com";' # # The exclude directive in "resolvconf" server section is partly recreated. # Known (configured) domains in the form "." or ".." are # added and removed automatically, unknown domains (also those not in # the format above) found in this directive are kept. # # The sample configuration file /etc/pdnsd/pdnsd.conf prepared to work # with resolvconf would look like this: # #global { # perm_cache=2048; # run_as="pdnsd"; # status_ctl = on; # Important to enable status control # run_ipv4=on; # par_queries=2; # How many servers are probed in parallel # interface = "lo"; # Interface on which the pdnsd listens #} # # 2. The last step is to configure dns configuration for /etc/resolv.conf. # # a) For " or "resolvconf" for global # section # * @RULE@ is @LABEL@ with translated characters '-' and '.' into '_'. ### ### # Main loop with label match - it will redirect the processing to # SED_EDIT_ONE_SERVER, when the label match is found. Special match is # for "resolvconf" label - the control flow is redirected to SED_ADDING to # allow adding new sections. # # To summarize: Old sections are edited as they appear in the file and new # sections are added before the "resolvconf" section. SED_LOOP=\ '/^[[:space:]]*server[[:space:]]*[\{]/ b server; p; d; :server; h; :server_loop; n; /^[[:space:]]*server[[:space:]]*[\{]/ { x; p; b server_loop; }; @MATCH_LABELS@ /^[[:space:]]*label[[:space:]]*=[[:space:]]*"resolvconf"/ { H; b adding; }; /^[[:space:]]*[\}]/ { H; x; p; d; }; H; b server_loop; ' ### # Match for one label with a jump to SED_EDIT_ONE_SERVER SED_MATCH_ONE_LABEL=\ '/^[[:space:]]*label[[:space:]]*=[[:space:]]*"@LABEL@"/ { H; x; b main_@RULE@; }; ' ### # Editing one server. New lines are put into @SETUP@, lines are composed # in function compose_lines(). After the new lines are added, all "preset", # "ip" and "include" options are removed (not printed). # # Sanity checks: Check if there is a second label or another server directive. # In both cases, there is some error in the file, so go to the beginning by # jumping to SED_LOOP's :server. SED_EDIT_ONE_SERVER=\ ':main_@RULE@; p; @SETUP@ :loop_@RULE@; n; /^[[:space:]]*server[[:space:]]*[\{]/ b server; /^[[:space:]]*label[[:space:]]*=/ b server; /^[[:space:]]*preset[[:space:]]*=/ b loop_@RULE@; /^[[:space:]]*ip[[:space:]]*=/ b loop_@RULE@; /^[[:space:]]*include[[:space:]]*=/ b loop_@RULE@; /^[[:space:]]*policy[[:space:]]*=/ b loop_@RULE@; /^[[:space:]]*exclude[[:space:]]*=/ b exclude_logic_@RULE; p; /^[[:space:]]*[\}]/ d; b loop_@RULE@; :exclude_logic_@RULE; @EXCLUDE_LOGIC@ b loop_@RULE@; ' ### # Add new servers. All lines composed by function compose_lines() are put into # @SETUP@. Then the control flow is returned to one special SED_EDIT_ONE_SERVER # section with label "resolvconf". SED_ADDING=\ ':adding; @SETUP@ x; b main_resolvconf; ' ### # Edit the domain list (include/exclude). All empty fields and matching domains # are removed. Unmaintained domains (not in resolvconf-) are kept. All # domains should be in a pipe (|) separated list and should begin, but not end # with a dot. The list is put into @DOMAINS@. The control flow continues, where # it ended in SED_EDIT_ONE_SERVER. # SED_DOMAIN_LIST_LOGIC=\ 'h; s/^([[:space:]]*@DIRECTIVE@[[:space:]]*=[[:space:]]*).*/\\1/; x; s/^[[:space:]]*@DIRECTIVE@[[:space:]]*=[[:space:]]*//; :@DIRECTIVE@_loop_@RULE@; /([[:space:]]*("[^"]"*|[^,;]*)[[:space:]]*,)*[[:space:]]*("(@DOMAINS@|)\.?"|(@DOMAINS@)\.?|,)[[:space:]]*[,;]/ { s/(([[:space:]]*("[^"]"*|[^,;]*)[[:space:]]*,)*[[:space:]]*)("(@DOMAINS@|)\.?"|(@DOMAINS@)\.?|,)[[:space:]]*([,;])/\\1\\7/; b @DIRECTIVE@_loop_@RULE@; }; s/^[,;]//g; /^[[:space:]]*$/ b @DIRECTIVE@_end_@RULE@; H; x; s/\\n//; p; :@DIRECTIVE@_end_@RULE@; ' ################################################################################ # Functions ### # char* [] uniqify(char* list[]) # # Uniqify the items in the list uniqify() { local result= while [ -n "$1" ] ; do case " ${result} " in *" $1 "*) ;; *) result="${result} $1" ;; esac shift done echo "${result# *}" } ### # char *make_pdnsd_label(char *domain) # # Translate domain name into pdnsd's label make_pdnsd_label() { local domain=$1 if [[ -n ${domain} ]] ; then echo -n "resolvconf-${domain}" else echo -n "resolvconf" fi } ### # char *make_sed_label(char *pdnsd_label) # # Translate pdnsd's label into sed's label make_sed_label() { local label="$1" label="${label//-/_}" label="${label//./_}" echo -n "${label}" } # char *compose_lines(...) # # Compose a sed command that prints lines compose_lines() { local line result for line in "$@" ; do result="${result}i\\\\\\n${line// /\\t}\\n" done echo "${result}" } ### # char *build_settings(char *nameservers, char *domains, char *directive) # # Builds configuration part @SETUP@ of sed script. The directive parameter denotes # if the domains are to be included ("include") or excluded ("exclude"). This # involves options like # # (1) # [nameserver list is empty] # preset=off; # # (2) # [domain list is empty] # preset=on; # ip="address","address"...; # # (3) # [directive=="include"] # preset=on; # ip="address","address"...; # include=".domain.",".domain."...; # policy=excluded; # # (4) # [directive=="exclude"] # preset=on; # ip="address","address"...; # exclude=".domain.",".domain."...; # policy=included; # # Note: Currently there will always be only one domain in "include" directive. # build_settings() { local ns="$1" domains="$2" directive="$3" if [[ -n ${ns} ]] ; then local x list_ns list_domains for x in ${ns} ; do list_ns="${list_ns},\"${x}\"" done list_ns="${list_ns#,}" if [[ -n ${domains} ]] ; then for x in ${domains} ; do list_domains="${list_domains},\".${x}.\"" done list_domains="${list_domains#,}" if [[ $directive == "include" ]]; then compose_lines \ " preset=on;" \ " ip=${list_ns};" \ " include=${list_domains};" \ " policy=excluded;" else compose_lines \ " preset=on;" \ " ip=${list_ns};" \ " exclude=${list_domains};" \ " policy=included;" fi else compose_lines \ " preset=on;" \ " ip=${list_ns};" fi else compose_lines \ " preset=off;" fi } ### # char *build_match_labels(char *domains...) # # Build the label match part of the sed script # build_match_labels() { local domain result label destination new_match for domain in "$@" ; do label="$(make_pdnsd_label "${domain}")" rule="$(make_sed_label "${label}")" new_match="${SED_MATCH_ONE_LABEL//@LABEL@/${label}}" new_match="${new_match//@RULE@/${rule}}" result="${result}${new_match}" done echo "${result}" } ### # char *build_domain_list_logic(char *domains, char *directive) # # Build a logic for changing (removing) domains from a directive. # build_domain_list_logic() { local domains="$1" directive="$2" local x domain_list logic # Domains should be pipe separated list for x in ${domains}; do x=".${x%.}" x="${x//./\.}" domain_list="${domain_list}|${x}" done domain_list="${domain_list#|}" if [[ -z ${domain_list} ]]; then logic="p;" else logic="${SED_DOMAIN_LIST_LOGIC//@DOMAINS@/${domain_list}}" logic="${logic//@DIRECTIVE@/${directive}}" fi echo "${logic}" } ### # char *build_edit_part(char *domain, char *nameservers, \ # char *add_domains, char *remove_domains, # char *directive) # # Build edit part of the sed script for a particular domain. Domain can be # empty in the case it is the "resolvconf" server part. # build_edit_part() { local domain="$1" nameservers="$2" add_domains="$3" remove_domains="$4" local directive="$5" local setup label rule logic result setup="$(build_settings "${nameservers}" "${add_domains}" "${directive}")" label="$(make_pdnsd_label "${domain}")" rule="$(make_sed_label "${label}")" logic="$(build_domain_list_logic "${remove_domains}" "${directive}")" result="${SED_EDIT_ONE_SERVER//@SETUP@/${setup}}" result="${result//@EXCLUDE_LOGIC@/${logic}}" result="${result//@RULE@/${rule}}" echo "${result}" } ### # char *get_domain_nameservers(char *domain, char *domain_config...) # # Get the list of nameservers belonging to one particular domain. # # Domain configuration is a space separated list of pair ,. # get_domain_nameservers() { local domain="$1" ns shift for x in "$@" ; do if [[ ${x%,*} == ${domain} ]] ; then ns="${ns} ${x#*,}" fi done ns="$(uniqify ${ns})" echo -n "${ns}" } ### # char *build_domain_edit_part(char *domain, char *domain_config...) # # Parse the list of domain configurations and build settings for one particular # domain for the sed script. # # Domain configuration is a space separated list of pair ,. # build_domain_edit_part() { local domain="$1" ns shift ns="$(get_domain_nameservers "${domain}" "$@")" build_edit_part "${domain}" "${ns}" "${domain}" "" "include" } ### # char *build_add_part(char *add, char *domains...) # # Build add part of the sed script for all domains that needs to be added # build_add_part() { local add="$1" x label rule add_part new_part result shift for x in ${add} ; do local domain="${x}" ns ns="$(get_domain_nameservers "${domain}" "$@")" label="$(make_pdnsd_label "${domain}")" rule="$(make_sed_label ${label})" new_part="$(compose_lines "server {" " label=\"${label}\";")" new_part="${new_part}$(build_settings "${ns}" "${domain}" "include")" new_part="${new_part}$(compose_lines "}" "")" add_part="${add_part}${new_part}" done result="${SED_ADDING//@SETUP@/${add_part}}" echo "${result}" } ### # char *build_sed_script(char *nameservers, char *domain_config, # char *change, char *add, # char *active_domains, char *known_domains) # # Build the full sed script from the list of nameservers, list of domains # (in format ,), list of changed domains, list of added domains, # list of activly used domains and a list of all known domains. # build_sed_script() { local ns="$1" domain_config="$2" change="$3" add="$4" local active_domains="$5" known_domains="$6" local match_labels="$(build_match_labels ${change})" local edit_changed x for x in ${change} ; do edit_changed="${edit_changed}$( \ build_domain_edit_part "${x}" ${domain_config})" done edit_changed="${edit_changed}$( \ build_edit_part "" "${ns}" "${active_domains}" "${known_domains}" "exclude")" local added added="$(build_add_part "${add}" ${domain_config})" local full full="${SED_LOOP//@MATCH_LABELS@/${match_labels}}" echo -ne "${full}" echo -ne "${edit_changed}" echo -ne "${added}" } ### # char *read_configured_domains(char *config_file) # # Reads labels of servers starting with resolvconf* from the configuration file. # read_configured_domains() { local config_file="$1" result result="\ $(sed -nre 's/^[[:space:]]+label=\"?resolvconf-([^;\"]*)\";.*/\1/p' \ ${config_file})" echo -n "${result}" } ### # void installation_check(char *config_file) # # Check if the pdnsd is installed and can be configured. Prepare also the file # for resolvconf. # installation_check() { local config_file="$1" if [[ -e ${config_file} ]] ; then if ! grep ${INSTALLATION_CHECK} ${config_file} &>/dev/null ; then echo -e "${COMMENT}" >> ${config_file} echo -e "\n${BASIC_SETTINGS}" >> ${config_file} fi return 0 else return 1 fi } ### # void initialization(char *config_file) # # Setup basic variables NAMESERVERS, DOMAINS an CONFIGURED_DOMAINS # initialization() { local config_file="$1" # Compatibility fix for new openresolv-2.0 if [[ -z ${NEWNS} && -n ${NAMESERVERS} ]]; then NEWDOMAIN=${DOMAINS//:/,} NEWSEARCH= NEWNS=${NAMESERVERS} DOMAINS= NAMESERVERS= SEARCH= fi for N in ${NEWNS} ; do NAMESERVERS="${NAMESERVERS} ${N}" done for N in ${NEWSEARCH} ; do NAMESERVERS="${NAMESERVERS} ${N#*,}" done for DN in ${NEWDOMAIN} ; do DOMAINS="${DOMAINS} ${DN%%,*}" done CONFIGURED_DOMAINS=$(read_configured_domains ${config_file}) NAMESERVERS=$(uniqify ${NAMESERVERS}) DOMAINS=$(uniqify ${DOMAINS}) CONFIGURED_DOMAINS=$(uniqify ${CONFIGURED_DOMAINS}) } ### # void find_changed_and_added(char *configured, char *domains) # # Find already configured and newly added domains. Sets variables # CHANGE_DOMAINS, ADD_DOMAINS and KNOWN_DOMAINS. # find_changed_and_added() { local configured="$1" domains="$2" x KNOWN_DOMAINS="${CONFIGURED_DOMAINS} ${DOMAINS}" # Find what has to be disabled for x in ${configured} ; do if [[ " ${domains} " != *" ${x} "* ]] ; then CHANGE_DOMAINS="${CHANGE_DOMAINS} ${x}" fi done # Find what has to be added for x in ${domains} ; do if [[ " ${configured} " != *" ${x} "* ]] ; then ADD_DOMAINS="${ADD_DOMAINS} ${x}" else CHANGE_DOMAINS="${CHANGE_DOMAINS} ${x}" fi done ADD_DOMAINS=$(uniqify ${ADD_DOMAINS}) CHANGE_DOMAINS=$(uniqify ${CHANGE_DOMAINS}) KNOWN_DOMAINS=$(uniqify ${KNOWN_DOMAINS}) } ### # bool make_configuration_change(char *config_file, char *backup_suffix, # char *sed_script) # # Applies any configuration change. Returns true, if there was a change. # make_configuration_change() { local config_file="$1" backup_suffix="$2" sed_script="$3" local old_config new_config old_config=$(< ${config_file}) # Sanity check: add '}' at the end of the file new_config=$( (echo -n "${old_config}" && echo -ne "\n}" ) | \ sed -nre "${sed_script}") # Now remove what we added new_config=${new_config%?\}} if [[ "${old_config}" != "${new_config}" ]] ; then cp ${config_file} ${config_file}${backup_suffix} echo "${new_config}" > "${config_file}" return 0 else return 1 fi } ################################################################################ # Main part # Check, if pdnsd configuration file is installed and possibly prepare it installation_check "${PDNSDCONFIG}" || exit 0 # Basic initialization of NAMESERVERS, DOMAINS and CONFIGURED_DOMAINS initialization "${PDNSDCONFIG}" find_changed_and_added "${CONFIGURED_DOMAINS}" "${DOMAINS}" sed_script="$(build_sed_script "${NAMESERVERS}" "${NEWDOMAIN}" \ "${CHANGE_DOMAINS}" "${ADD_DOMAINS}" \ "${DOMAINS}" "${KNOWN_DOMAINS}")" # Check if the config changed if make_configuration_change "${PDNSDCONFIG}" "${BACKUPSUFFIX}" "${sed_script}" ; then # Checks for running pdnsd [ -x /usr/sbin/pdnsd-ctl ] || exit 0 [ -e /var/cache/pdnsd/pdnsd.status ] || exit 0 # Reload config files /usr/sbin/pdnsd-ctl config &>/dev/null fi exit 0