#!/bin/sh
##############################################################################
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 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.
#
# Copyright (C) 2016 Eric Luehrsen
#
##############################################################################
#
# This crosses over to the dnsmasq UCI file "dhcp" and parses it for fields
# that will allow Unbound to request local host DNS of dnsmasq. We need to look
# at the interfaces in "dhcp" and get their subnets. The Unbound conf syntax
# makes this a little difficult. First in "server:" we need to create private
# zones for the domain and PTR records. Then we need to create numerous
# "forward:" clauses to forward those zones to dnsmasq.
#
##############################################################################

# while useful (sh)ellcheck is pedantic and noisy
# shellcheck disable=1091,2002,2004,2034,2039,2086,2094,2140,2154,2155

DM_D_WAN_FQDN=0

DM_LIST_KNOWN_ZONES="invalid"
DM_LIST_TRN_ZONES=""
DM_LIST_LOCAL_DATA=""
DM_LIST_PRZ_DATA=""
DM_LIST_LOCAL_PTR=""
DM_LIST_FWD_PORTS=""
DM_LIST_FWD_ZONES=""

##############################################################################

create_local_zone() {
  local target="$1"
  local partial domain found

  case $DM_LIST_TRN_ZONES in
    *"${target}"*)
      found=1
      ;;

    *)
      case $target in
        [A-Za-z0-9]*.[A-Za-z0-9]*)
          found=0
          ;;

        *) # no dots
          found=1
          ;;
      esac
  esac


  if [ $found -eq 0 ] ; then
    # New Zone! Bundle local-zones: by first two name tiers "abcd.tld."
    partial=$( echo "$target" | awk -F. '{ j=NF ; i=j-1; print $i"."$j }' )
    DM_LIST_TRN_ZONES="$DM_LIST_TRN_ZONES $partial"
    DM_LIST_KNOWN_ZONES="$DM_LIST_KNOWN_ZONES $partial"
  fi
}

##############################################################################

create_host_record_from_domain() {
  local cfg="$1"
  local ip name debug_ip

  # basefiles dhcp "domain" clause which means host A, AAAA, and PRT record
  config_get ip   "$cfg" ip
  config_get name "$cfg" name


  if [ -n "$name" ] && [ -n "$ip" ] ; then
    create_local_zone "$name"


    case $ip in
      fe[89ab][0-9a-f]:*|169.254.*)
        debug_ip="$ip@$name"
        ;;

      [1-9a-f]*:*[0-9a-f])
        DM_LIST_LOCAL_DATA="$DM_LIST_LOCAL_DATA $name.@@300@@IN@@AAAA@@$ip"
        DM_LIST_LOCAL_PTR="$DM_LIST_LOCAL_PTR $ip@@300@@$name"
        ;;

      [1-9]*.*[0-9])
        DM_LIST_LOCAL_DATA="$DM_LIST_LOCAL_DATA $name.@@300@@IN@@A@@$ip"
        DM_LIST_LOCAL_PTR="$DM_LIST_LOCAL_PTR $ip@@300@@$name"
        ;;
    esac
  fi
}

##############################################################################

create_host_record_from_host() {
  local cfg="$1"
  local dns ip name

  # basefiles dhcp "host" clause which means host A and PTR records
  config_get dns  "$cfg" dns 0
  config_get ip   "$cfg" ip
  config_get name "$cfg" name


  if [ -n "$name" ] && [ -n "$ip" ] && [ $dns -eq 1 ] ; then
    case $name in
      *.*)
        # domain present, do nothing
        ;;
      *)
        name="$name.$UB_TXT_DOMAIN"
        ;;
    esac


    create_local_zone "$name"
    DM_LIST_LOCAL_DATA="$DM_LIST_LOCAL_DATA $name.@@300@@IN@@A@@$ip"
    DM_LIST_LOCAL_PTR="$DM_LIST_LOCAL_PTR $ip@@300@@$name"
  fi
}

##############################################################################

create_mx_record() {
  local cfg="$1"
  local domain relay pref record

  # Insert a static MX record
  config_get domain "$cfg" domain
  config_get relay  "$cfg" relay
  config_get pref   "$cfg" pref 10


  if [ -n "$domain" ] && [ -n "$relay" ] ; then
    create_local_zone "$domain"
    record="$domain.@@300@@IN@@MX@@$pref@@$relay."
    DM_LIST_LOCAL_DATA="$DM_LIST_LOCAL_DATA $record"
  fi
}

##############################################################################

create_srv_record() {
  local cfg="$1"
  local srv target port class weight record

  # Insert a static SRV record such as SIP server
  config_get srv    "$cfg" srv
  config_get target "$cfg" target
  config_get port   "$cfg" port
  config_get class  "$cfg" class 10
  config_get weight "$cfg" weight 10


  if [ -n "$srv" ] && [ -n "$target" ] && [ -n "$port" ] ; then
    create_local_zone "$srv"
    record="$srv.@@300@@IN@@SRV@@$class@@$weight@@$port@@$target."
    DM_LIST_LOCAL_DATA="$DM_LIST_LOCAL_DATA $record"
  fi
}

##############################################################################

create_cname_record() {
  local cfg="$1"
  local cname target record

  # Insert static CNAME record
  config_get cname  "$cfg" cname
  config_get target "$cfg" target


  # For cnames with a local data target the A RR is not resolved and missing
  # in the response. As most applications don't send another query and fail,
  # these entries are placed in a rpz zone instead.
  if [ -n "$cname" ] && [ -n "$target" ] ; then
    record="${DM_LIST_LOCAL_DATA#*${target}.@@*@@IN@@A@@}"
    if [ "$record" == "$DM_LIST_LOCAL_DATA" ]; then
      # Target is not a local data record => local data can be used
      create_local_zone "$cname"
      record="$cname.@@300@@IN@@CNAME@@$target."
      DM_LIST_LOCAL_DATA="$DM_LIST_LOCAL_DATA $record"

    else
      # Target is a local data record => use rpz zone

      # Add A RR at the end if still not present
      record="$target@@A@@${record%% *}"
      if [ "${DM_LIST_PRZ_DATA}" == "${DM_LIST_PRZ_DATA#*${record}}" ]; then
        DM_LIST_PRZ_DATA="$DM_LIST_PRZ_DATA $record"
      fi

      # Add CNAME at the beginning
      record="$cname@@CNAME@@$target."
      DM_LIST_PRZ_DATA="$record $DM_LIST_PRZ_DATA"
    fi
  fi
}

##############################################################################

dnsmasq_local_zone() {
  local cfg="$1"
  local fwd_port fwd_domain wan_fqdn

  # dnsmasq domain and interface assignment settings will control config
  config_get fwd_domain "$cfg" domain
  config_get fwd_port "$cfg" port
  config_get wan_fqdn "$cfg" add_wan_fqdn


  if [ -n "$wan_fqdn" ] ; then
    DM_D_WAN_FQDN=$wan_fqdn
  fi


  if [ -n "$fwd_domain" ] && [ -n "$fwd_port" ] \
  && [ ! ${fwd_port:-53} -eq 53 ] ; then
    # dnsmasq localhost listening ports (possible multiple instances)
    DM_LIST_FWD_PORTS="$DM_LIST_FWD_PORTS $fwd_port"
    DM_LIST_FWD_ZONES="$DM_LIST_FWD_ZONES $fwd_domain"
  fi
}

##############################################################################

dnsmasq_local_arpa() {
  local ifarpa ifsubnet


  if [ -n "$UB_LIST_NETW_LAN" ] ; then
    for ifsubnet in $UB_LIST_NETW_LAN ; do
      ifarpa=$( domain_ptr_any "${ifsubnet#*@}" )
      DM_LIST_FWD_ZONES="$DM_LIST_FWD_ZONES $ifarpa"
    done
  fi


  if [ -n "$UB_LIST_NETW_WAN" ] && [ $DM_D_WAN_FQDN -gt 0 ] ; then
    for ifsubnet in $UB_LIST_NETW_WAN ; do
      ifarpa=$( domain_ptr_any "${ifsubnet#*@}" )
      DM_LIST_FWD_ZONES="$DM_LIST_FWD_ZONES $ifarpa"
    done
  fi
}

##############################################################################

dnsmasq_inactive() {
  local record


  if [ $UB_D_EXTRA_DNS -gt 0 ] ; then
    # Parasite from the uci.dhcp.domain clauses
    DM_LIST_KNOWN_ZONES="$DM_LIST_KNOWN_ZONES $UB_TXT_DOMAIN"
    config_load dhcp
    config_foreach create_host_record_from_domain domain
    config_foreach create_host_record_from_host host


    if [ $UB_D_EXTRA_DNS -gt 1 ] ; then
      config_foreach create_srv_record srvhost
      config_foreach create_mx_record mxhost
    fi


    if [ $UB_D_EXTRA_DNS -gt 2 ] ; then
      config_foreach create_cname_record cname
    fi


    {
      echo "# $UB_SRVMASQ_CONF generated by UCI"
      if [ -n "$DM_LIST_TRN_ZONES" ] ; then
        for record in $DM_LIST_TRN_ZONES ; do
          echo "  local-zone: $record transparent"
        done
        echo
      fi
      if [ -n "$DM_LIST_LOCAL_DATA" ] ; then
        for record in $DM_LIST_LOCAL_DATA ; do
          echo "  local-data: \"${record//@@/ }\""
        done
        echo
      fi
      if [ -n "$DM_LIST_LOCAL_PTR" ] ; then
        for record in $DM_LIST_LOCAL_PTR ; do
          echo "  local-data-ptr: \"${record//@@/ }\""
        done
        echo
      fi
    } > $UB_SRVMASQ_CONF

    if [ -n "$DM_LIST_PRZ_DATA" ] ; then
      {
        echo '$ORIGIN cname.rpz.localhost; generated by UCI'
	echo ""
        for record in $DM_LIST_PRZ_DATA; do
          echo "${record//@@/ }"
        done
      } > $UB_RPZCNAME_CONF

      {
        echo "# $UB_EXTMASQ_CONF generated by UCI"
        echo "rpz:"
        echo "  name: cname.rpz.localhost"
        echo "  zonefile: $UB_RPZCNAME_CONF"
	echo ""
      } > $UB_EXTMASQ_CONF
    fi
  fi
}

##############################################################################

dnsmasq_active() {
  # Look at dnsmasq settings
  config_load dhcp
  # Zone for DHCP / SLAAC-PING DOMAIN
  config_foreach dnsmasq_local_zone dnsmasq
  # Zone for DHCP / SLAAC-PING ARPA
  dnsmasq_local_arpa


  if [ -n "$DM_LIST_FWD_PORTS" ] && [ -n "$DM_LIST_FWD_ZONES" ] ; then
    if [ $UB_B_DNS_ASSIST -lt 1 ] ; then
      {
        # Forward to dnsmasq on same host for DHCP lease hosts
        echo "# $UB_SRVMASQ_CONF generated by UCI"
        echo "  do-not-query-localhost: no"
        echo
      } > $UB_SRVMASQ_CONF

    else
      echo > $UB_SRVMASQ_CONF
    fi

    echo "# $UB_EXTMASQ_CONF generated by UCI" > $UB_EXTMASQ_CONF


    for fwd_domain in $DM_LIST_FWD_ZONES ; do
      {
        # This creates a domain with local privledges
        echo "  domain-insecure: $fwd_domain"
        echo "  private-domain: $fwd_domain"
        echo "  local-zone: $fwd_domain transparent"
        echo
      } >> $UB_SRVMASQ_CONF

      {
        # This is derived from dnsmasq local domain and dhcp service subnets
        echo "forward-zone:"
        echo "  name: $fwd_domain"
        echo "  forward-first: no"
        for port in $DM_LIST_FWD_PORTS ; do
          echo "  forward-addr: 127.0.0.1@$port"
        done
        echo
      } >> $UB_EXTMASQ_CONF
    done
  fi
}

##############################################################################

dnsmasq_link() {
  if [ "$UB_D_DHCP_LINK" = "dnsmasq" ] ; then
    dnsmasq_active

  else
    dnsmasq_inactive
  fi
}

##############################################################################

