#!/bin/sh

# PROVIDE: pfsense_tailscaled
# REQUIRE: tailscaled

# part of pfSense (https://www.pfsense.org)
# Copyright (c) 2022-2025 Rubicon Communications, LLC (Netgate)
# All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

. /etc/rc.subr 

name="pfsense_tailscaled"

load_rc_config $name

# defaults parameter values
: ${pfsense_tailscaled_acceptdns_enable:="YES"}
: ${pfsense_tailscaled_acceptroutes_enable:="NO"}
: ${pfsense_tailscaled_advertiseroutes:=""}
: ${pfsense_tailscaled_exitnode_enable:="NO"}
: ${pfsense_tailscaled_loginserver:="https://controlplane.tailscale.com"}
: ${pfsense_tailscaled_snat_subnet_routes:="NO"}

# source the tailscaled rc configuration too
load_rc_config tailscaled

# handle the --auth-key parameter
pfsense_tailscaled_up_flags="--auth-key=${pfsense_tailscaled_authkey}"

# handle the --login-server parameter
pfsense_tailscaled_up_flags="${pfsense_tailscaled_up_flags} --login-server=${pfsense_tailscaled_loginserver}"

# handle the --advertise-exit-node parameter
if checkyesno pfsense_tailscaled_exitnode_enable; then
	pfsense_tailscaled_up_flags="${pfsense_tailscaled_up_flags} --advertise-exit-node"
else
	pfsense_tailscaled_up_flags="${pfsense_tailscaled_up_flags} --advertise-exit-node=false"
fi

# handle the --accept-routes parameter
if checkyesno pfsense_tailscaled_acceptroutes_enable; then
	pfsense_tailscaled_up_flags="${pfsense_tailscaled_up_flags} --accept-routes"
else
	pfsense_tailscaled_up_flags="${pfsense_tailscaled_up_flags} --accept-routes=false"
fi

# handle the --acept-dns parameter
if checkyesno pfsense_tailscaled_acceptdns_enable; then
	pfsense_tailscaled_up_flags="${pfsense_tailscaled_up_flags} --accept-dns"
else
	pfsense_tailscaled_up_flags="${pfsense_tailscaled_up_flags} --accept-dns=false"
fi

# handle the --advertise-routes parameter
pfsense_tailscaled_up_flags="${pfsense_tailscaled_up_flags} --advertise-routes=${pfsense_tailscaled_advertiseroutes}"

# This is currently on available on Linux (see https://github.com/tailscale/tailscale/issues/5573)
# handle the –-snat-subnet-routes parameter
#if checkyesno pfsense_tailscaled_snat_subnet_routes; then
#	pfsense_tailscaled_up_flags="${pfsense_tailscaled_up_flags} --snat-subnet-routes"
#else
#	pfsense_tailscaled_up_flags="${pfsense_tailscaled_up_flags} --snat-subnet-routes=false"
#fi

extra_commands="clean" 

start_cmd="${name}_start"
start_postcmd="${name}_poststart"
stop_cmd="${name}_stop"
clean_cmd="${name}_clean"

# some shared configuration
tailscale_tun_dev="tailscale0"
tailscale_tun_dev_group="Tailscale"
tailscale_bin="/usr/local/bin/tailscale"
tailscaled_rcfile="/usr/local/etc/rc.d/tailscaled"
tailscale_tun_dev_wait=5

# useful bits
tailscaled_state_file="tailscaled.state"
tailscaled_vendor_state_dir="/var/db/tailscale"

# rc command hook
pfsense_tailscaled_start() {
	# ensure if_tuntap is loaded
	check_load_if_tuntap || return 1

	# ensure tailscaled state exists on persistent storage
	tailscaled_migrate_state

	# now we can safely start tailscaled
	tailscaled_start
}

# rc command hook
pfsense_tailscaled_poststart() {
	# wait for the tun device to come up
	wait_for_tun_dev ${tailscale_tun_dev_wait} || return 1

	# add the tun device to the interface group
	tun_dev_add_group

	# bring up Tailscale
	tailscale_up

	# reload the packet filter
	pfsense_reload_filter
}

# rc command hook
pfsense_tailscaled_stop() {
	tailscaled_stop
}

# rc command hook
pfsense_tailscaled_clean() {
	# stop tailscaled daemon
	tailscaled_stop

	# delete the state file
	/bin/rm -f ${tailscaled_state_dir}/${tailscaled_state_file} >/dev/null 2>&1
}

# bring up tailscale using the generated up flags from the configuration
tailscale_up() {
	logger -s -t tailscale "Bringing up ${tailscale_tun_dev} with ${pfsense_tailscaled_up_flags}"
	${tailscale_bin} up ${pfsense_tailscaled_up_flags} || {
		logger -s -t "Unable to bring up ${tailscale_tun_dev}. Check authentication."
	}
}

# utility function to check if a kernel module is loaded and load it if not loaded
check_load_kmod() {
	local _mod="${1}"
	/sbin/kldstat -q -m ${_mod} || /sbin/kldload ${_mod} >/dev/null 2>&1
}

# wrapper specific for if_tuntap module
check_load_if_tuntap() {
	check_load_kmod if_tuntap
}

# migrate state cache to configuration defined location
tailscaled_migrate_state() {
	local _src_state_file=${tailscaled_vendor_state_dir}/${tailscaled_state_file}
	local _dst_state_file=${tailscaled_state_dir}/${tailscaled_state_file}

	# nothing to do if these are equal, so bail out
	[ "${tailscaled_vendor_state_dir}" = "${tailscaled_state_dir}" ] && return 0

	# no state file to migrate, so bail out
	[ ! -f ${_src_state_file} ] && return 0

	# create new state directory
	if [ ! -d ${tailscaled_state_dir} ]; then
		/bin/mkdir -p ${tailscaled_state_dir} || (
			logger -s -t tailscale "Unable to initialize state directory ${tailscaled_state_dir}"
			return 1
		)
	fi

	# migrate the src state file to the new location
	/bin/cp ${_src_state_file} ${_dst_state_file} || (
		logger -s -t tailscale "Unable to migrate state file ${_src_state_file} to ${_dst_state_file}"
		return 1
	)

	# delete the src state file
	/bin/rm -f ${_src_state_file} >/dev/null 2>&1

	# all done
	logger -s -t tailscale "Migrated state file ${_src_state_file} to ${_dst_state_file}"
}

# determine if the tun device exists
tun_dev_exists() {
	/sbin/ifconfig ${tailscale_tun_dev} >/dev/null 2>&1
}

# determine if the tun device does not exist
tun_dev_not_exists() {
	!(tun_dev_exists)
}

# needed because daemon(8) forks and daemonizes long before the tun device is actually created
wait_for_tun_dev() {
	local _maxwaited="${1}"

	logger -s -t tailscale "Waiting for device ${tailscale_tun_dev}"
	_waited=0
	while tun_dev_not_exists || {
		logger -s -t tailscale "Found device ${tailscale_tun_dev}"
		return 0
	}; do
		[ ${_waited} -gt ${_maxwaited} ] && break
		sleep 1
		_waited=$((_waited+1))
	done

	logger -s -t tailscale "Unable to find device ${tailscale_tun_dev}"
	return 1
}

# silently check tailscaled status, little sugar
tailscaled_status() {
	run_rc_script ${tailscaled_rcfile} status >/dev/null 2>&1
}

# sugar
tailscaled_running() {
	tailscaled_status
}

# moar sugar
tailscaled_not_running() {
	!(tailscaled_running)
}

# start tailscaled if not running, proxy through to upstream rcfile
tailscaled_start() {
	tailscaled_running && return 1;
	run_rc_script ${tailscaled_rcfile} start
}

# stop tailscaled if running, proxy through to upstream rcfile
tailscaled_stop() {
	tailscaled_not_running && return 1
	run_rc_script ${tailscaled_rcfile} stop
}

# restart tailscaled, proxy through to upstream rcfile
tailscaled_restart() {
	run_rc_script ${tailscaled_rcfile} restart
}

# add the tun device to the interface group
tun_dev_add_group() {
	/sbin/ifconfig ${tailscale_tun_dev} group ${tailscale_tun_dev_group} >/dev/null 2>&1 && {
		logger -s -t tailscale "Added ${tailscale_tun_dev} to interface group ${tailscale_tun_dev_group}"
		return 0
	}

	logger -s -t tailscale "Unable to add ${tailscale_tun_dev} to interface group ${tailscale_tun_dev_group}"
	return 1
}

# reload pfsense packet filter rules
pfsense_reload_filter() {
	debug "Reloading pfSense packet filter"
	/etc/rc.filter_configure_sync
}

run_rc_command "$1"
