#!/bin/sh # Copyright 1999-2010 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 printf "%s" "${result# *}" } ### # char *make_pdnsd_label(char *domain) # # Translate domain name into pdnsd's label make_pdnsd_label() { local domain="$1" if [ -n "${domain}" ] ; then printf "%s" "resolvconf-${domain}" else printf "%s" "resolvconf" fi } ### # char *replace(char *text, char *search, char *replace) # # Replaces all the found "search" occurences in the "text" with the "replace". replace() { local text="$1" search="$2" replace="$3" local pre post="${text}" result="" while [ x"${pre}" != x"${text}" ] ; do post="${text}" pre="${text%%$search*}" post="${text#*$search}" if [ x"${pre}" != x"${text}" ] ; then result="${result}${pre}${replace}" text="${post}" fi done printf "%s" "${result}${post}" } ### # char *make_sed_label(char *pdnsd_label) # # Translate pdnsd's label into sed's label make_sed_label() { local label="$1" label="$(replace "${label}" "-" "_")" label="$(replace "${label}" "." "_")" printf "%s" "${label}" } # char *compose_lines(...) # # Compose a sed command that prints lines compose_lines() { local line result for line in "$@" ; do result="${result}i\\\\\\n$(replace "${line}" " " "\\t")\\n" done printf "%s" "${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 [ x"$directive" = x"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="$(replace "${SED_MATCH_ONE_LABEL}" "@LABEL@" "${label}")" new_match="$(replace "${new_match}" "@RULE@" "${rule}")" result="${result}${new_match}\n" done printf "%s" "${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="$(replace "${x}" "." "\\.")" domain_list="${domain_list}|${x}" done domain_list="${domain_list#|}" if [ -z "${domain_list}" ]; then logic="p;" else logic="$(replace "${SED_DOMAIN_LIST_LOGIC}" "@DOMAINS@" "${domain_list}")" logic="$(replace "${logic}" "@DIRECTIVE@" "${directive}")" fi printf "%s" "${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="$(replace "${SED_EDIT_ONE_SERVER}" "@SETUP@" "${setup}")" result="$(replace "${result}" "@EXCLUDE_LOGIC@" "${logic}")" result="$(replace "${result}" "@RULE@" "${rule}")" printf "%s\n" "${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"${x%,*}" = x"${domain}" ] ; then ns="${ns} ${x#*,}" fi done ns="$(uniqify ${ns})" printf "%s" "${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="$(replace "${SED_ADDING}" "@SETUP@" "${add_part}")" printf "%s" "${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="$(replace "${SED_LOOP}" "@MATCH_LABELS@" "${match_labels}")" printf "%b\n" "${full}" printf "%b\n" "${edit_changed}" printf "%b" "${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})" printf "%s" "${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 2>&1; then printf "%s\n" "${COMMENT}" >> "${config_file}" printf "\n%s\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}" -a -n "${NAMESERVERS}" ]; then NEWDOMAIN=$(replace "${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 case " ${domains} " in *" ${x} "*) ;; *) CHANGE_DOMAINS="${CHANGE_DOMAINS} ${x}" ;; esac done # Find what has to be added for x in ${domains} ; do case " ${configured} " in *" ${x} "*) CHANGE_DOMAINS="${CHANGE_DOMAINS} ${x}" ;; *) ADD_DOMAINS="${ADD_DOMAINS} ${x}" ;; esac 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="$(cat "${config_file}")" # Sanity check: add '}' at the end of the file new_config=$( (printf "%s" "${old_config}" && printf "\n}" ) | \ sed -nre "${sed_script}") # Now remove what we added new_config=${new_config%?\}} if [ x"${old_config}" != x"${new_config}" ] ; then cp ${config_file} ${config_file}${backup_suffix} printf "%s\n" "${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 2>&1 fi exit 0