#!/bin/bash set -e RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' CONFIG_DIR="/etc/sing-box" CONFIG_MERGE_DIR="$CONFIG_DIR/config.d" CONFIG_BASE_FILE="$CONFIG_MERGE_DIR/10-base.json" CONFIG_ROUTE_FILE="$CONFIG_MERGE_DIR/route.json" CONFIG_OUTBOUNDS_FILE="$CONFIG_MERGE_DIR/outbound.json" LEGACY_CONFIG_OUTBOUNDS_FILE="$CONFIG_MERGE_DIR/20-outbounds.json" WORK_DIR="/var/lib/sing-box" BINARY_PATH="/usr/local/bin/sing-box" SERVICE_NAME="singbox" SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service" LEGACY_SERVICE_NAMES=("ganclient" "sing-box") RELEASE_BASE_URL="${RELEASE_BASE_URL:-https://s3.cloudyun.top/downloads/singbox}" PUBLISHED_SCRIPT_URL="${PUBLISHED_SCRIPT_URL:-https://s3.cloudyun.top/downloads/singbox/install.sh}" V2BX_DETECTED=0 V2BX_CONFIG_PATH="" UNINSTALL_V2BX_DEFAULT="${UNINSTALL_V2BX_DEFAULT:-n}" SCRIPT_VERSION="${SCRIPT_VERSION:-v1.3.0}" declare -a V2BX_IMPORTED_NODE_IDS=() declare -a EXISTING_IMPORTED_NODE_IDS=() declare -a NODE_IDS=() EXISTING_INSTALL=0 EXISTING_CONFIG_SOURCE="" PROMPT_FOR_CONFIG=1 CONFIG_BACKUP_DIR="" echo -e "${GREEN}Welcome to singbox Release Installation Script${NC}" echo -e "${GREEN}Script version: ${SCRIPT_VERSION}${NC}" echo -e "${YELLOW}Published install script: ${PUBLISHED_SCRIPT_URL}${NC}" if [[ $EUID -ne 0 ]]; then echo -e "${RED}This script must be run as root${NC}" exit 1 fi OS="$(uname -s)" if [[ "$OS" != "Linux" ]]; then echo -e "${RED}This install script currently supports Linux only. Current OS: ${OS}${NC}" exit 1 fi if [[ ! -t 0 ]]; then if [[ -r /dev/tty ]]; then exec 3/dev/null 2>&1; then V2BX_DETECTED=1 fi if find_v2bx_config; then V2BX_DETECTED=1 fi return 0 } detect_existing_installation() { if [[ -x "$BINARY_PATH" || -f "$SERVICE_FILE" || -f "$CONFIG_BASE_FILE" || -f "$CONFIG_ROUTE_FILE" || -f "$CONFIG_OUTBOUNDS_FILE" || -f "$LEGACY_CONFIG_OUTBOUNDS_FILE" || -f "$CONFIG_DIR/config.json" ]]; then EXISTING_INSTALL=1 fi if [[ -f "$CONFIG_BASE_FILE" ]]; then EXISTING_CONFIG_SOURCE="$CONFIG_BASE_FILE" elif [[ -f "$CONFIG_DIR/config.json" ]]; then EXISTING_CONFIG_SOURCE="$CONFIG_DIR/config.json" fi if [[ "$EXISTING_INSTALL" -eq 1 ]]; then echo -e "${YELLOW}Detected existing sing-box installation. Switching to update flow...${NC}" if [[ -n "$EXISTING_CONFIG_SOURCE" ]]; then echo -e "${YELLOW}Existing config source: ${EXISTING_CONFIG_SOURCE}${NC}" fi fi } load_existing_install_defaults() { if [[ -z "$EXISTING_CONFIG_SOURCE" || ! -f "$EXISTING_CONFIG_SOURCE" ]]; then return 1 fi echo -e "${YELLOW}Loading defaults from existing sing-box config...${NC}" EXISTING_IMPORTED_NODE_IDS=() local parsed="" if command -v python3 >/dev/null 2>&1; then parsed="$(python3 - "$EXISTING_CONFIG_SOURCE" <<'PY' import json import sys path = sys.argv[1] with open(path, "r", encoding="utf-8") as f: data = json.load(f) xboard = {} for service in data.get("services") or []: if isinstance(service, dict) and service.get("type") == "xboard": xboard = service break print(f"PANEL_URL={xboard.get('panel_url', '')}") print(f"PANEL_TOKEN={xboard.get('key', '')}") node_id = xboard.get("node_id") if node_id not in (None, ""): print(f"NODE_ID={node_id}") for node in xboard.get("nodes") or []: if isinstance(node, dict): current_node_id = node.get("node_id") if current_node_id not in (None, ""): print(f"NODE_ID={current_node_id}") servers = ((data.get("dns") or {}).get("servers") or []) dns_server = servers[0] if servers else {} if isinstance(dns_server, dict): print(f"DNS_MODE={dns_server.get('type', '')}") print(f"DNS_SERVER={dns_server.get('server', '')}") print(f"DNS_SERVER_PORT={dns_server.get('server_port', '')}") PY )" elif command -v jq >/dev/null 2>&1; then parsed="$(jq -r ' ([.services[]? | select(.type == "xboard")] | .[0] // {}) as $xboard | "PANEL_URL=" + ($xboard.panel_url // ""), "PANEL_TOKEN=" + ($xboard.key // ""), (if ($xboard.node_id // empty) != empty then "NODE_ID=" + ($xboard.node_id | tostring) else empty end), (($xboard.nodes // [])[]? | "NODE_ID=" + (.node_id | tostring)), (.dns.servers[0] // {}) as $dns | "DNS_MODE=" + ($dns.type // ""), "DNS_SERVER=" + ($dns.server // ""), "DNS_SERVER_PORT=" + (($dns.server_port // "") | tostring) ' "$EXISTING_CONFIG_SOURCE" 2>/dev/null || true)" else echo -e "${YELLOW}Neither python3 nor jq found, unable to auto-load existing config.${NC}" return 1 fi if [[ -z "$parsed" ]]; then return 1 fi local parsed_line while IFS= read -r parsed_line; do parsed_line="$(sanitize_value "$parsed_line")" case "$parsed_line" in PANEL_URL=*) PANEL_URL="$(sanitize_value "${parsed_line#PANEL_URL=}")" ;; PANEL_TOKEN=*) PANEL_TOKEN="$(sanitize_value "${parsed_line#PANEL_TOKEN=}")" ;; NODE_ID=*) append_unique_node_id_from_existing "${parsed_line#NODE_ID=}" ;; DNS_MODE=*) DNS_MODE="$(echo "${parsed_line#DNS_MODE=}" | tr '[:upper:]' '[:lower:]')" ;; DNS_SERVER=*) DNS_SERVER="$(sanitize_value "${parsed_line#DNS_SERVER=}")" ;; DNS_SERVER_PORT=*) DNS_SERVER_PORT="$(sanitize_value "${parsed_line#DNS_SERVER_PORT=}")" ;; esac done <<< "$parsed" if [[ "${#EXISTING_IMPORTED_NODE_IDS[@]}" -gt 0 ]]; then NODE_IDS=("${EXISTING_IMPORTED_NODE_IDS[@]}") NODE_ID="${EXISTING_IMPORTED_NODE_IDS[0]}" fi if [[ -n "${DNS_MODE:-}" ]]; then DNS_MODE="$(sanitize_value "$DNS_MODE")" fi if [[ -n "${PANEL_URL:-}" && -n "${PANEL_TOKEN:-}" && "${#NODE_IDS[@]}" -gt 0 && ( "${DNS_MODE:-}" == "local" || ( "${DNS_MODE:-}" == "udp" && -n "${DNS_SERVER:-}" && -n "${DNS_SERVER_PORT:-}" ) ) ]]; then echo -e "${YELLOW}Loaded existing config: PanelURL=${PANEL_URL}, NodeIDs=$(IFS=,; echo "${NODE_IDS[*]}"), DNS=${DNS_MODE}${NC}" return 0 fi echo -e "${YELLOW}Existing config detected, but some fields are incomplete. The installer will ask for the missing values.${NC}" return 1 } append_unique_node_id_from_existing() { local normalized_value local node_id_part local existing_node_id normalized_value="$(normalize_node_id_input "$1")" if [[ -z "$normalized_value" ]]; then return 0 fi read -r -a NORMALIZED_NODE_ID_PARTS <<< "$normalized_value" for node_id_part in "${NORMALIZED_NODE_ID_PARTS[@]}"; do if ! [[ "$node_id_part" =~ ^[0-9]+$ ]]; then continue fi for existing_node_id in "${EXISTING_IMPORTED_NODE_IDS[@]}"; do if [[ "$existing_node_id" == "$node_id_part" ]]; then node_id_part="" break fi done if [[ -n "$node_id_part" ]]; then EXISTING_IMPORTED_NODE_IDS+=("$node_id_part") fi done } ensure_config_backup_dir() { if [[ -n "$CONFIG_BACKUP_DIR" ]]; then return 0 fi CONFIG_BACKUP_DIR="$CONFIG_DIR/backup/$(date +%Y%m%d-%H%M%S)" mkdir -p "$CONFIG_BACKUP_DIR" echo -e "${YELLOW}Backing up existing configuration to ${CONFIG_BACKUP_DIR}${NC}" } backup_path_if_exists() { local path="$1" if [[ ! -e "$path" ]]; then return 0 fi ensure_config_backup_dir cp -a "$path" "$CONFIG_BACKUP_DIR/" } archive_legacy_config_json() { local legacy_config_path="$CONFIG_DIR/config.json" if [[ ! -f "$legacy_config_path" ]]; then return 0 fi ensure_config_backup_dir mv "$legacy_config_path" "$CONFIG_BACKUP_DIR/config.json.migrated" echo -e "${YELLOW}Legacy single-file config has been archived to ${CONFIG_BACKUP_DIR}/config.json.migrated${NC}" } extract_legacy_config_sections() { local source_path="$1" local route_written=0 local outbound_written=0 if [[ ! -f "$source_path" ]]; then return 1 fi if command -v python3 >/dev/null 2>&1; then local parsed parsed="$(python3 - "$source_path" "$CONFIG_ROUTE_FILE" "$CONFIG_OUTBOUNDS_FILE" <<'PY' import json import sys source_path, route_path, outbounds_path = sys.argv[1:4] with open(source_path, "r", encoding="utf-8") as f: data = json.load(f) route_written = 0 if isinstance(data.get("route"), dict): with open(route_path, "w", encoding="utf-8") as f: json.dump({"route": data["route"]}, f, ensure_ascii=False, indent=2) f.write("\n") route_written = 1 outbound_written = 0 if isinstance(data.get("outbounds"), list): with open(outbounds_path, "w", encoding="utf-8") as f: json.dump({"outbounds": data["outbounds"]}, f, ensure_ascii=False, indent=2) f.write("\n") outbound_written = 1 print(f"ROUTE_WRITTEN={route_written}") print(f"OUTBOUNDS_WRITTEN={outbound_written}") PY )" while IFS= read -r parsed_line; do case "$parsed_line" in ROUTE_WRITTEN=1) route_written=1 ;; OUTBOUNDS_WRITTEN=1) outbound_written=1 ;; esac done <<< "$parsed" elif command -v jq >/dev/null 2>&1; then if jq -e '.route | type == "object"' "$source_path" >/dev/null 2>&1; then jq '{route: .route}' "$source_path" > "$CONFIG_ROUTE_FILE" route_written=1 fi if jq -e '.outbounds | type == "array"' "$source_path" >/dev/null 2>&1; then jq '{outbounds: .outbounds}' "$source_path" > "$CONFIG_OUTBOUNDS_FILE" outbound_written=1 fi fi if [[ "$route_written" -eq 1 || "$outbound_written" -eq 1 ]]; then echo -e "${YELLOW}Extracted legacy config sections from ${source_path}${NC}" return 0 fi return 1 } write_default_route_config() { cat > "$CONFIG_ROUTE_FILE" < "$CONFIG_OUTBOUNDS_FILE" </dev/null 2>&1; then local parsed parsed="$(python3 - "$V2BX_CONFIG_PATH" <<'PY' import json import sys path = sys.argv[1] with open(path, "r", encoding="utf-8") as f: data = json.load(f) nodes = data.get("Nodes") or [] node = nodes[0] if nodes else {} api_host = node.get("ApiHost", "") if node else "" api_key = node.get("ApiKey", "") if node else "" print(f"API_HOST={api_host or ''}") print(f"API_KEY={api_key or ''}") for entry in nodes: node_id = entry.get("NodeID", "") if node_id is None: node_id = "" print(f"NODE_ID={node_id}") PY )" if [[ -n "$parsed" ]]; then local parsed_line while IFS= read -r parsed_line; do parsed_line="$(sanitize_value "$parsed_line")" case "$parsed_line" in API_HOST=*) if [[ -z "${PANEL_URL:-}" ]]; then PANEL_URL="$(sanitize_value "${parsed_line#API_HOST=}")" fi ;; API_KEY=*) if [[ -z "${PANEL_TOKEN:-}" ]]; then PANEL_TOKEN="$(sanitize_value "${parsed_line#API_KEY=}")" fi ;; NODE_ID=*) append_unique_node_id "${parsed_line#NODE_ID=}" ;; esac done <<< "$parsed" fi elif command -v jq >/dev/null 2>&1; then local parsed parsed="$(jq -r '(.Nodes[0].ApiHost // "" | "API_HOST=" + .), (.Nodes[0].ApiKey // "" | "API_KEY=" + .), (.Nodes[]?.NodeID // "" | tostring | "NODE_ID=" + .)' "$V2BX_CONFIG_PATH" 2>/dev/null || true)" if [[ -n "$parsed" ]]; then local parsed_line while IFS= read -r parsed_line; do parsed_line="$(sanitize_value "$parsed_line")" case "$parsed_line" in API_HOST=*) if [[ -z "${PANEL_URL:-}" ]]; then PANEL_URL="$(sanitize_value "${parsed_line#API_HOST=}")" fi ;; API_KEY=*) if [[ -z "${PANEL_TOKEN:-}" ]]; then PANEL_TOKEN="$(sanitize_value "${parsed_line#API_KEY=}")" fi ;; NODE_ID=*) append_unique_node_id "${parsed_line#NODE_ID=}" ;; esac done <<< "$parsed" fi else echo -e "${YELLOW}Neither python3 nor jq found, skipping automatic V2bX config import.${NC}" fi if [[ -z "${NODE_ID:-}" && "${#V2BX_IMPORTED_NODE_IDS[@]}" -gt 0 ]]; then NODE_ID="${V2BX_IMPORTED_NODE_IDS[0]}" fi if [[ "${#V2BX_IMPORTED_NODE_IDS[@]}" -gt 0 ]]; then echo -e "${YELLOW}Imported defaults from V2bX config: ApiHost=${PANEL_URL:-}, NodeIDs=$(IFS=,; echo "${V2BX_IMPORTED_NODE_IDS[*]}")${NC}" else echo -e "${YELLOW}Imported defaults from V2bX config: ApiHost=${PANEL_URL:-}, NodeIDs=${NC}" fi return 0 } stop_v2bx_if_present() { if [[ "$V2BX_DETECTED" -ne 1 ]]; then return 0 fi if ! command -v v2bx >/dev/null 2>&1; then echo -e "${YELLOW}V2bX config detected but 'v2bx' command not found, skipping stop/disable.${NC}" return 0 fi echo -e "${YELLOW}Detected V2bX, stopping and disabling it before continuing...${NC}" v2bx stop || true v2bx disable || true } prompt_uninstall_v2bx() { if [[ "$V2BX_DETECTED" -ne 1 ]]; then return 0 fi if ! command -v v2bx >/dev/null 2>&1; then return 0 fi read -u 3 -p "Detected V2bX. Uninstall it now? [${UNINSTALL_V2BX_DEFAULT}]: " INPUT_UNINSTALL_V2BX local uninstall_v2bx_answer uninstall_v2bx_answer=${INPUT_UNINSTALL_V2BX:-$UNINSTALL_V2BX_DEFAULT} if [[ "$uninstall_v2bx_answer" =~ ^([yY][eE][sS]|[yY]|1|true|TRUE)$ ]]; then echo -e "${YELLOW}Running: v2bx uninstall${NC}" v2bx uninstall else echo -e "${YELLOW}Keeping existing V2bX installation.${NC}" fi } download_binary() { echo -e "${YELLOW}Downloading sing-box release binary...${NC}" echo -e "${YELLOW}Target: ${DOWNLOAD_TARGET}${NC}" echo -e "${YELLOW}URL: ${DOWNLOAD_URL}${NC}" if command -v curl >/dev/null 2>&1; then if ! curl -fL "${DOWNLOAD_URL}" -o "${TMP_BINARY}"; then echo -e "${RED}Failed to download release binary with curl.${NC}" rm -f "${TMP_BINARY}" exit 1 fi elif command -v wget >/dev/null 2>&1; then if ! wget -O "${TMP_BINARY}" "${DOWNLOAD_URL}"; then echo -e "${RED}Failed to download release binary with wget.${NC}" rm -f "${TMP_BINARY}" exit 1 fi else echo -e "${RED}Neither curl nor wget is installed.${NC}" rm -f "${TMP_BINARY}" exit 1 fi install -m 0755 "${TMP_BINARY}" "${BINARY_PATH}" rm -f "${TMP_BINARY}" if [[ ! -x "${BINARY_PATH}" ]]; then echo -e "${RED}Binary install failed: ${BINARY_PATH} not executable.${NC}" exit 1 fi echo -e "${GREEN}sing-box downloaded and installed to ${BINARY_PATH}${NC}" } cleanup_legacy_service() { echo -e "${YELLOW}Cleaning up legacy services if present...${NC}" for legacy_service_name in "${LEGACY_SERVICE_NAMES[@]}"; do if [[ "$legacy_service_name" == "$SERVICE_NAME" ]]; then continue fi legacy_service_file="/etc/systemd/system/${legacy_service_name}.service" if systemctl list-unit-files | grep -q "^${legacy_service_name}\.service"; then systemctl stop "${legacy_service_name}" 2>/dev/null || true systemctl disable "${legacy_service_name}" 2>/dev/null || true fi if [[ -f "$legacy_service_file" ]]; then rm -f "$legacy_service_file" fi if [[ -L "/etc/systemd/system/multi-user.target.wants/${legacy_service_name}.service" ]]; then rm -f "/etc/systemd/system/multi-user.target.wants/${legacy_service_name}.service" fi done } detect_v2bx detect_existing_installation stop_v2bx_if_present mkdir -p "$CONFIG_DIR" mkdir -p "$CONFIG_MERGE_DIR" mkdir -p "$WORK_DIR" if [[ -f ".env" ]]; then echo -e "${YELLOW}Loading configuration from .env...${NC}" source .env fi load_v2bx_defaults || true PANEL_URL="$(sanitize_value "${PANEL_URL:-}")" PANEL_TOKEN="$(sanitize_value "${PANEL_TOKEN:-}")" NODE_ID="$(sanitize_value "${NODE_ID:-}")" DNS_MODE="$(sanitize_value "${DNS_MODE:-}")" DNS_SERVER="$(sanitize_value "${DNS_SERVER:-}")" DNS_SERVER_PORT="$(sanitize_value "${DNS_SERVER_PORT:-}")" ENABLE_PROXY_PROTOCOL_HINT="$(sanitize_value "${ENABLE_PROXY_PROTOCOL_HINT:-n}")" if [[ "$EXISTING_INSTALL" -eq 1 ]]; then if load_existing_install_defaults; then PROMPT_FOR_CONFIG=0 fi fi download_binary cleanup_legacy_service if [[ "$PROMPT_FOR_CONFIG" -eq 1 ]]; then read -u 3 -p "Enter Panel URL [${PANEL_URL}]: " INPUT_URL PANEL_URL="$(sanitize_value "${INPUT_URL:-$PANEL_URL}")" read -u 3 -p "Enter Panel Token (Node Key) [${PANEL_TOKEN}]: " INPUT_TOKEN PANEL_TOKEN="$(sanitize_value "${INPUT_TOKEN:-$PANEL_TOKEN}")" read -u 3 -p "This node is behind an L4 proxy/LB that sends PROXY protocol? [${ENABLE_PROXY_PROTOCOL_HINT:-n}]: " INPUT_PROXY_PROTOCOL ENABLE_PROXY_PROTOCOL_HINT="$(sanitize_value "${INPUT_PROXY_PROTOCOL:-${ENABLE_PROXY_PROTOCOL_HINT:-n}}")" NODE_IDS=() i=1 while true; do DEFAULT_NODE_ID="" if [[ "$i" -le "${#EXISTING_IMPORTED_NODE_IDS[@]}" ]]; then DEFAULT_NODE_ID="${EXISTING_IMPORTED_NODE_IDS[$((i-1))]}" elif [[ "$i" -le "${#V2BX_IMPORTED_NODE_IDS[@]}" ]]; then DEFAULT_NODE_ID="${V2BX_IMPORTED_NODE_IDS[$((i-1))]}" elif [[ "$i" -eq 1 && -n "${NODE_ID:-}" ]]; then DEFAULT_NODE_ID="$NODE_ID" fi if [[ -n "$DEFAULT_NODE_ID" ]]; then read -u 3 -p "Enter Node ID for node #$i [${DEFAULT_NODE_ID}] (type N/NO to finish): " INPUT_ID else read -u 3 -p "Enter Node ID for node #$i (press Enter or type N/NO to finish): " INPUT_ID fi CURRENT_NODE_ID="$(sanitize_value "${INPUT_ID:-$DEFAULT_NODE_ID}")" if [[ -z "$DEFAULT_NODE_ID" && -z "$CURRENT_NODE_ID" && "${#NODE_IDS[@]}" -gt 0 ]]; then break fi if [[ "$CURRENT_NODE_ID" =~ ^([nN]|[nN][oO])$ ]]; then if [[ "${#NODE_IDS[@]}" -eq 0 ]]; then echo -e "${RED}At least one Node ID is required${NC}" exit 1 fi break fi CURRENT_NODE_ID="$(normalize_node_id_input "$CURRENT_NODE_ID")" if [[ -z "$CURRENT_NODE_ID" ]]; then echo -e "${RED}Node ID is required for node #$i${NC}" exit 1 fi read -r -a CURRENT_NODE_ID_PARTS <<< "$CURRENT_NODE_ID" if [[ "${#CURRENT_NODE_ID_PARTS[@]}" -eq 0 ]]; then echo -e "${RED}Node ID is required for node #$i${NC}" exit 1 fi for CURRENT_NODE_ID_PART in "${CURRENT_NODE_ID_PARTS[@]}"; do if ! [[ "$CURRENT_NODE_ID_PART" =~ ^[0-9]+$ ]]; then echo -e "${RED}Node ID must be a positive integer, got: ${CURRENT_NODE_ID_PART}${NC}" exit 1 fi NODE_IDS+=("$CURRENT_NODE_ID_PART") done ((i++)) done DNS_MODE_DEFAULT=${DNS_MODE:-udp} read -u 3 -p "Enter DNS mode [${DNS_MODE_DEFAULT}] (udp/local): " INPUT_DNS_MODE DNS_MODE=$(echo "${INPUT_DNS_MODE:-$DNS_MODE_DEFAULT}" | tr '[:upper:]' '[:lower:]') case "$DNS_MODE" in udp) DNS_SERVER_DEFAULT=${DNS_SERVER:-1.1.1.1} DNS_SERVER_PORT_DEFAULT=${DNS_SERVER_PORT:-53} read -u 3 -p "Enter DNS server [${DNS_SERVER_DEFAULT}]: " INPUT_DNS_SERVER DNS_SERVER="$(sanitize_value "${INPUT_DNS_SERVER:-$DNS_SERVER_DEFAULT}")" read -u 3 -p "Enter DNS server port [${DNS_SERVER_PORT_DEFAULT}]: " INPUT_DNS_SERVER_PORT DNS_SERVER_PORT="$(sanitize_value "${INPUT_DNS_SERVER_PORT:-$DNS_SERVER_PORT_DEFAULT}")" ;; local) DNS_SERVER="" DNS_SERVER_PORT="" ;; *) echo -e "${RED}Unsupported DNS mode: $DNS_MODE. Supported values: udp, local${NC}" exit 1 ;; esac else echo -e "${YELLOW}Reusing existing install settings for update.${NC}" fi NODE_COUNT=${#NODE_IDS[@]} case "${DNS_MODE:-udp}" in udp) if [[ -z "$DNS_SERVER" ]]; then echo -e "${RED}DNS server is required in udp mode${NC}" exit 1 fi if ! [[ "$DNS_SERVER_PORT" =~ ^[0-9]+$ ]] || [[ "$DNS_SERVER_PORT" -lt 1 ]] || [[ "$DNS_SERVER_PORT" -gt 65535 ]]; then echo -e "${RED}DNS server port must be an integer between 1 and 65535${NC}" exit 1 fi DNS_SERVER_JSON=$(cat < "$CONFIG_BASE_FILE" < "$SERVICE_FILE" <