#!/bin/bash
#
# dns-updown - add/remove openvpn provided DNS information
#
# Copyright (C) 2024-2026 OpenVPN Inc <sales@openvpn.net>
#
# SPDX-License-Identifier: GPL-2.0
#
# Add/remove openvpn DNS settings from the env into/from
# the system. Supported backends in this order:
#
#   * systemd-resolved
#   * resolvconf
#   * /etc/resolv.conf file
#
# Example env from openvpn (not all are always applied):
#
#   dns_vars_file /tmp/openvpn_dvf_58b95c0c97b2db43afb5d745f986c53c.tmp
#
#      or
#
#   dev tun0
#   script_type dns-up
#   dns_search_domain_1 mycorp.in
#   dns_search_domain_2 eu.mycorp.com
#   dns_server_1_address_1 192.168.99.254
#   dns_server_1_address_2 fd00::99:53
#   dns_server_1_port_1 53
#   dns_server_1_port_2 53
#   dns_server_1_resolve_domain_1 mycorp.in
#   dns_server_1_resolve_domain_2 eu.mycorp.com
#   dns_server_1_dnssec yes
#   dns_server_1_transport DoH
#   dns_server_1_sni dns.mycorp.in
#

[ -z "${dns_vars_file}" ] || . "${dns_vars_file}"

function do_resolved_servers {
    local sni=""
    local transport_var=dns_server_${n}_transport
    local sni_var=dns_server_${n}_sni
    [ "${!transport_var}" = "DoT" ] && sni="#${!sni_var}"

    local i=1
    local addrs=""
    while :; do
        local addr_var=dns_server_${n}_address_${i}
        local addr="${!addr_var}"
        [ -n "$addr" ] || break

        local port_var=dns_server_${n}_port_${i}
        if [ -n "${!port_var}" ]; then
            if [[ "$addr" =~ : ]]; then
                addr="[$addr]"
            fi
            addrs+="${addr}:${!port_var}${sni} "
        else
            addrs+="${addr}${sni} "
        fi
        i=$((i+1))
    done

    resolvectl dns "$dev" $addrs
}

function do_resolved_domains {
    local list=""
    for domain_var in ${!dns_search_domain_*}; do
        list+="${!domain_var} "
    done
    local domain_var=dns_server_${n}_resolve_domain_1
    if [ -z "${!domain_var}" ]; then
        resolvectl default-route "$dev" true
        list+="~."
    else
        resolvectl default-route "$dev" false
        local i=1
        while :; do
            domain_var=dns_server_${n}_resolve_domain_${i}
            [ -n "${!domain_var}" ] || break
            # Add as split domain (~ prefix), if it doesn't already exist
            [[ "$list" =~ (^| )"${!domain_var}"( |$) ]] \
                || list+="~${!domain_var} "
            i=$((i+1))
        done
    fi

    resolvectl domain "$dev" $list
}

function do_resolved_dnssec {
    local dnssec_var=dns_server_${n}_dnssec
    if [ "${!dnssec_var}" = "optional" ]; then
        resolvectl dnssec "$dev" allow-downgrade
    elif [ "${!dnssec_var}" = "yes" ]; then
        resolvectl dnssec "$dev" true
    else
        resolvectl dnssec "$dev" false
    fi
}

function do_resolved_dnsovertls {
    local transport_var=dns_server_${n}_transport
    if [ "${!transport_var}" = "DoT" ]; then
        resolvectl dnsovertls "$dev" true
    else
        resolvectl dnsovertls "$dev" false
    fi
}

function do_resolved {
    [[ "$(readlink /etc/resolv.conf)" =~ systemd ]] || return 1

    n=1
    while :; do
        local addr_var=dns_server_${n}_address_1
        [ -n "${!addr_var}" ] || {
            echo "setting DNS failed, no compatible server profile"
            return 1
        }

        # Skip server profiles which require DNS-over-HTTPS
        local transport_var=dns_server_${n}_transport
        [ -n "${!transport_var}" -a "${!transport_var}" = "DoH" ] || break

        n=$((n+1))
    done

    if [ "$script_type" = "dns-up" ]; then
        echo "setting DNS using resolvectl"
        do_resolved_servers
        do_resolved_domains
        do_resolved_dnssec
        do_resolved_dnsovertls
    else
        echo "unsetting DNS using resolvectl"
        resolvectl revert "$dev"
    fi

    return 0
}

function only_standard_server_ports {
    local i=1
    while :; do
        local addr_var=dns_server_${n}_address_${i}
        [ -n "${!addr_var}" ] || return 0

        local port_var=dns_server_${n}_port_${i}
        [ -z "${!port_var}" -o "${!port_var}" = "53" ] || return 1

        i=$((i+1))
    done
}

function resolv_conf_compat_profile {
    local n=1
    while :; do
        local server_addr_var=dns_server_${n}_address_1
        [ -n "${!server_addr_var}" ] || {
            echo "setting DNS failed, no compatible server profile"
            exit 1
        }

        # Skip server profiles which require DNSSEC,
        # secure transport or use a custom port
        local dnssec_var=dns_server_${n}_dnssec
        local transport_var=dns_server_${n}_transport
        [ -z "${!transport_var}" -o "${!transport_var}" = "plain" ] \
            && [ -z "${!dnssec_var}" -o "${!dnssec_var}" = "no" ] \
            && only_standard_server_ports && break

        n=$((n+1))
    done
    return $n
}

function do_resolvconf {
    [ -x /sbin/resolvconf ] || return 1

    resolv_conf_compat_profile
    local n=$?

    if [ "$script_type" = "dns-up" ]; then
        echo "setting DNS using resolvconf"
        local domains=""
        for domain_var in ${!dns_search_domain_*}; do
            domains+="${!domain_var} "
        done
        {
            local i=1
            local maxns=3
            while [ "${i}" -le "${maxns}" ]; do
                local addr_var=dns_server_${n}_address_${i}
                [ -n "${!addr_var}" ] || break
                echo "nameserver ${!addr_var}"
                i=$((i+1))
            done
            [ -z "$domains" ] || echo "search $domains"
        } | /sbin/resolvconf -a "$dev"
    else
        echo "unsetting DNS using resolvconf"
        /sbin/resolvconf -d "$dev"
    fi

    return 0
}

function do_resolv_conf_file {
    conf=/etc/resolv.conf
    test -e "$conf" || exit 1

    resolv_conf_compat_profile
    local n=$?

    if [ "$script_type" = "dns-up" ]; then
        echo "setting DNS using resolv.conf file"

        local addr1_var=dns_server_${n}_address_1
        local addr2_var=dns_server_${n}_address_2
        local addr3_var=dns_server_${n}_address_3
        text="### openvpn ${dev} begin ###\n"
        text="${text}nameserver ${!addr1_var}\n"
        test -z "${!addr2_var}" || text="${text}nameserver ${!addr2_var}\n"
        test -z "${!addr3_var}" || text="${text}nameserver ${!addr3_var}\n"

        test -z "$dns_search_domain_1" || {
            for i in $(seq 1 6); do
                eval domains=\"$domains\$dns_search_domain_${i} \" || break
            done
            text="${text}search $domains\n"
        }
        text="${text}### openvpn ${dev} end ###"

        sed -i "1i${text}" "$conf"
    else
        echo "unsetting DNS using resolv.conf file"
        sed -i "/### openvpn ${dev} begin ###/,/### openvpn ${dev} end ###/d" "$conf"
    fi

    return 0
}

do_resolved || do_resolvconf || do_resolv_conf_file || {
    echo "setting DNS failed, no method succeeded"
    exit 1
}
