#!/bin/sh # # This script creates or configures a GIF (Generic Interface) tunnel interface based on settings # in /cf/conf/config.xml,in pfSense systems. It ensures that 'ifconfig' output # for the GIF interface aligns with the pfSense GUI, including proper tunnel configuration and # RUNNING status when an outer IPv6 address is present. # # If no interface name is provided, defaults to 'gif0'. # ----------------------------------------------------------------------------------------------------------- # Configuration set -eu # Enable strict error handling: exit on error (-e) and treat unset variables as errors (-u). GIF_INTERFACE_ID="${1:-gif0}" # Set the GIF interface name, defaulting to 'gif0' if not provided as an argument. AFTR_HOST_FQDN="aftr.fra.purtel.com" # Address Family Transition Router of ISP Deustche Giganetz PFSENSE_CONFIG_FILE="/cf/conf/config.xml" # Define the path to the pfSense configuration file. CONFIG_BACKUP="/cf/conf/config.xml.bak.$(date +%Y%m%d%H%M%S)" DEBUG_OUTPUT_TO_PFSENSE_LOG=0 # 1 to enable logging to syslog DEBUG_OUTPUT_TO_CONSOLE=1 # 1 to enable console output # ----------------------------------------------------------------------------------------------------------- # Helper Functions # ----------------------------------------------- # Write Data to console or to pfsense logger (or both) debug_out() { local message="StartupGifAfterBoot-->DEBUG: $1" [ "$DEBUG_OUTPUT_TO_PFSENSE_LOG" = "1" ] && logger -t StartupGifAfterBoot "$message" [ "$DEBUG_OUTPUT_TO_CONSOLE" = "1" ] && echo "$message" } # ----------------------------------------------- # Helper functions for XML parsing, IP handling, and interface queries. # Check if 'xmllint' is installed; required for XML parsing. Exit with error if missing. command -v xmllint >/dev/null 2>&1 || { debug_out "xmllint required"; exit 1; } # ----------------------------------------------- # Extract a string value from the XML config using an XPath expression. # Parameters: $1 - XPath expression. # Returns: The value or an empty string if not found or an error occurs. get_xml() { xpath="$1" xmllint --xpath "string($xpath)" "$PFSENSE_CONFIG_FILE" 2>/dev/null || echo "" } # ----------------------------------------------- # Count the number of XML nodes matching an XPath expression. # Parameters: $1 - XPath expression. # Returns: The count or 0 if an error occurs. xml_count() { xpath="$1" xmllint --xpath "count($xpath)" "$PFSENSE_CONFIG_FILE" 2>/dev/null || echo 0 } # ----------------------------------------------- # Convert a CIDR prefix length to a dotted decimal netmask (e.g., 24 to 255.255.255.0). # Parameters: $1 - CIDR value. # Returns: The netmask or an empty string if the input is invalid. cidr2dotted() { cidr="$1" # Return empty if CIDR is unset. [ -n "$cidr" ] || { debug_out ""; return; } # Check if CIDR is a valid number; return empty if not. case "$cidr" in *[!0-9]*|'') debug_out ""; return ;; esac # Validate CIDR range (0-32); return empty if invalid. if [ "$cidr" -lt 0 ] || [ "$cidr" -gt 32 ]; then debug_out ""; return; fi out="" # Process each octet (4 total). for _ in 1 2 3 4; do # If CIDR >= 8, use 255 for the octet. if [ "$cidr" -ge 8 ]; then out="${out}255." cidr=$((cidr-8)) else # Calculate partial octet if CIDR > 0, else use 0. if [ "$cidr" -gt 0 ]; then oct=$((256 - (1 << (8 - cidr)))) else oct=0 fi out="${out}${oct}." cidr=0 fi done # Remove trailing dot and output the netmask. printf "%s" "${out%?}" } # ----------------------------------------------- # Check if an address is IPv6 based on the presence of colons. # Parameters: $1 - IP address. # Returns: 0 if IPv6, 1 if not. is_ipv6() { case "$1" in *:*) return 0;; *) return 1;; esac } # ----------------------------------------------- # Get the first global (non-link-local) IPv6 address from an interface. # Parameters: $1 - Interface name. # Returns: The IPv6 address or empty if none found or interface doesn't exist. get_if_ipv6_global() { ifname="$1" # Check if the interface exists; return empty if not. if ! ifconfig "$ifname" >/dev/null 2>&1; then debug_out ""; return; fi # Extract the first non-link-local (not fe80) IPv6 address from ifconfig output. ifconfig "$ifname" | awk '/inet6/ && $2 !~ /^fe80/ {print $2; exit}' | cut -d'/' -f1 || true } # ----------------------------------------------- # Get any IPv6 address (including link-local) from an interface. # Parameters: $1 - Interface name. # Returns: The first IPv6 address or empty if none found or interface doesn't exist. get_if_ipv6_any() { ifname="$1" # Check if the interface exists; return empty if not. if ! ifconfig "$ifname" >/dev/null 2>&1; then debug_out ""; return; fi # Extract the first IPv6 address from ifconfig output. ifconfig "$ifname" | awk '/inet6/ {print $2; exit}' | cut -d'/' -f1 || true } # ----------------------------------------------- # Check, if an interface is up and running check_interface_ready() { local iface=$1 # Check if interface is UP and RUNNING (flags) if ifconfig "$iface" 2>/dev/null | grep -q "<.*UP.*RUNNING.*>"; then # Check if interface has inet (IPv4) or inet6 if ifconfig "$iface" 2>/dev/null | grep -q -e "inet " -e "inet6 "; then return 0 # interface is ready fi fi return 1 # not ready } # ----------------------------------------------------------------------------------------------------------- # Reentrancy or double start protection # ----------------------------------------------- # Reentrancy or double start protection PIDFILE="/var/run/$(basename $0).pid" if [ -f "$PIDFILE" ]; then debug_out "Another instance is already running. Exiting." exit 0 fi echo $$ > "$PIDFILE" trap 'rm -f "$PIDFILE"; exit' INT TERM EXIT # ----------------------------------------------------------------------------------------------------------- # Main functionality # ----------------------------------------------- # Read Configuration from config.xml # Extract relevant GIF and interface settings from the pfSense XML configuration. # Define XPath to locate the GIF configuration for the specified interface. GIF_XPATH="/pfsense/gifs/gif[gifif='$GIF_INTERFACE_ID']" # Verify that a GIF entry exists for the interface; exit if not found. [ "$(xml_count "$GIF_XPATH")" != "0" ] || { debug_out "No entry for $GIF_INTERFACE_ID in $PFSENSE_CONFIG_FILE"; exit 1; } # Extract tunnel local address (inner local IP). INNER_LOCAL="$(get_xml "$GIF_XPATH/tunnel-local-addr")" # Extract tunnel remote address (inner remote IP). INNER_REMOTE="$(get_xml "$GIF_XPATH/tunnel-remote-addr")" # Extract tunnel remote network CIDR (inner netmask). INNER_REMOTE_NET="$(get_xml "$GIF_XPATH/tunnel-remote-net")" # Extract outer remote address (remote tunnel endpoint). OUTER_REMOTE="$(get_xml "$GIF_XPATH/remote-addr")" # Extract logical parent interface name. PARENT_LOGICAL="$(get_xml "$GIF_XPATH/if")" # Extract GIF description. GIF_DESCR="$(get_xml "$GIF_XPATH/descr")" # Define XPath for the interface configuration. IFACE_XPATH="/pfsense/interfaces/*[if='$GIF_INTERFACE_ID']" # Extract interface description. IFACE_DESCR="$(get_xml "$IFACE_XPATH/descr")" # Extract MTU setting. IFACE_MTU="$(get_xml "$IFACE_XPATH/mtu")" # Extract MSS setting (though not used later in the script). IFACE_MSS="$(get_xml "$IFACE_XPATH/mss")" # Extract the descriptive name of the gif interface GIF_DESCRIPTIVE_NAME="$(get_xml "$GIF_XPATH/descr")" # Set description: use interface description if available, else fall back to GIF description. [ -n "$IFACE_DESCR" ] && DESC="$IFACE_DESCR" || DESC="$GIF_DESCR" # Determine the physical parent interface. PHYS_PARENT="" # If a logical parent is specified and exists in the interfaces section. if [ -n "$PARENT_LOGICAL" ] && [ "$(xml_count "/pfsense/interfaces/$PARENT_LOGICAL")" != "0" ]; then # Get the physical interface name from the logical parent's config. PHYS_PARENT="$(get_xml "/pfsense/interfaces/$PARENT_LOGICAL/if")" else # Use the logical parent as the physical parent if no mapping exists. PHYS_PARENT="$PARENT_LOGICAL" fi # Display extracted configuration for debugging and verification. debug_out "Config:" debug_out " gif node: $GIF_XPATH" debug_out " gifif: $GIF_INTERFACE_ID" debug_out " parent logical: $PARENT_LOGICAL -> physical: $PHYS_PARENT" debug_out " outer remote: $OUTER_REMOTE" debug_out " inner local: $INNER_LOCAL" debug_out " inner remote: $INNER_REMOTE" debug_out " inner /CIDR: $INNER_REMOTE_NET" debug_out " GIF Interface name: $DESC" debug_out " GIF tunnel description: $GIF_DESCRIPTIVE_NAME" debug_out " mtu: $IFACE_MTU" # ----------------------------------------------- # Ensure the outer remote address is set; exit if missing. if [ -z "$OUTER_REMOTE" ]; then debug_out "ERROR: remote-addr (outer remote) missing in gif config" exit 1 fi # ----------------------------------------------- # Wait for All Interfaces to be Ready debug_out "Waiting for necessary interfaces to come up for GIF startup..." # List expected interfaces (excluding the GIF you're about to create) #EXPECTED_INTERFACES=$(xmllint --xpath "//interfaces/*[not(enable) or enable != 'false']/if[not(starts-with(text(),'gif'))]/text()" /cf/conf/config.xml 2>/dev/null | tr '\n' ' ') EXPECTED_INTERFACES="igc3 igc2 pppoe0 lagg0.10 lagg0.20 lagg0.30 lagg0.40 lagg0.50 lagg0.60" max_wait=120 # 2 minutes timeout wait_time=0 while [ $wait_time -lt $max_wait ]; do all_ready=true for iface in $EXPECTED_INTERFACES; do if ! check_interface_ready "$iface"; then all_ready=false debug_out "Interface $iface not ready yet..." break fi done if [ "$all_ready" = true ]; then debug_out "All interfaces are ready for GIF Startup" break fi sleep 5 wait_time=$((wait_time + 5)) done if [ $wait_time -ge $max_wait ]; then debug_out "Warning: Timeout waiting for all interfaces" fi # ----------------------------------------------- # Determine and Wait for Local Outer Address # Determine the local outer IP address, waiting for an IPv6 address if needed. # Initialize the local outer address variable. LOCAL_OUTER="" # If the outer remote address is IPv6. if is_ipv6 "$OUTER_REMOTE"; then # Set retry parameters for waiting on a global IPv6 address. retries=25 attempt=0 # Attempt to find a global IPv6 address, retrying up to 25 times. while [ "$attempt" -lt "$retries" ]; do attempt=$((attempt+1)) LOCAL_OUTER="$(get_if_ipv6_global "$PHYS_PARENT")" # Break if a global IPv6 address is found. if [ -n "$LOCAL_OUTER" ]; then break; fi # debug_out retry attempt and wait 5 seconds. debug_out "Waiting for global IPv6 on $PHYS_PARENT (attempt $attempt/$retries)..." sleep 5 done # If no global IPv6 is found, fall back to any IPv6 (including link-local). if [ -z "$LOCAL_OUTER" ]; then debug_out "No global IPv6 — falling back to any IPv6 (possibly link-local)" LOCAL_OUTER="$(get_if_ipv6_any "$PHYS_PARENT")" # If a link-local address (fe80), append the interface scope. if [ -n "$LOCAL_OUTER" ]; then case "$LOCAL_OUTER" in fe80:*) LOCAL_OUTER="${LOCAL_OUTER}%${PHYS_PARENT}" ;; esac fi fi # Exit if no IPv6 address is found. if [ -z "$LOCAL_OUTER" ]; then debug_out "ERROR: no IPv6 address found on $PHYS_PARENT" exit 1 fi else # For IPv4: extract the first non-loopback IPv4 address from the parent interface. if ifconfig "$PHYS_PARENT" >/dev/null 2>&1; then LOCAL_OUTER="$(ifconfig "$PHYS_PARENT" | awk '/inet / && $2 != "127.0.0.1" {print $2; exit}')" fi # Exit if no IPv4 address is found. [ -n "$LOCAL_OUTER" ] || { debug_out "ERROR: no IPv4 on $PHYS_PARENT"; exit 1; } fi # ----------------------------------------------------------------------------------------------------------- # ----------------------------------------------- # AFTR IP Updater debug_out "=== pfSense DS-Lite AFTR Address Updater ===" # ----------------------------------------------- #Get tunnel remote address debug_out "Checking AFTR address for: $GIF_DESCRIPTIVE_NAME" AFTR_IPv6=$(dig +short AAAA "$AFTR_HOST_FQDN" | head -n1 | tr -d '\r') if [ -z "$AFTR_IPv6" ]; then debug_out "Error: Could not resolve AFTR FQDN address for '$AFTR_HOST_FQDN'" exit 1 fi debug_out "FQDN resolved AFTR address: $AFTR_IPv6" # ----------------------------------------------- #Check and handle potential updated AFTR IP if [ "$OUTER_REMOTE" = "$AFTR_IPv6" ]; then debug_out "OK: Addresses match - no update needed" else debug_out "Mismatch: Addresses differ - update required" debug_out "Current: $OUTER_REMOTE | resolved: $AFTR_IPv6" # Backup and update configuration debug_out "Creating backup: $CONFIG_BACKUP" cp "$PFSENSE_CONFIG_FILE" "$CONFIG_BACKUP" || { debug_out "Error: Backup failed"; rm -f "$PIDFILE"; exit 1; } debug_out "Updating configuration..." # BSD sed -i '' usage; escape possible slashes in addresses by using | delimiter sed -i '' "s|$GIF_REMOTE|$AFTR_IPv6|g" "$PFSENSE_CONFIG_FILE" NEW_AFTR_IP="$(get_xml "$GIF_XPATH/remote-addr")" if [ "$NEW_AFTR_IP" != "$AFTR_IPv6" ]; then debug_out "Error: Update failed, restoring backup" cp "$CONFIG_BACKUP" "$PFSENSE_CONFIG_FILE" exit 1 fi debug_out "OK: Configuration updated successfully" debug_out "Old: $GIF_REMOTE | New: $NEW_AFTR_IP" fi debug_out "=== AFTR IP Update Complete ===" # debug_out the determined local outer address and parent interface. debug_out "Using outer local: $LOCAL_OUTER (parent $PHYS_PARENT)" # ----------------------------------------------------------------------------------------------------------- # ----------------------------------------------- # Destroy and Recreate GIF Interface # Destroy any existing GIF interface and create a new one. # Check if the GIF interface already exists. if ifconfig "$GIF_INTERFACE_ID" >/dev/null 2>&1; then # Attempt to destroy the existing interface. debug_out "Destroying existing $GIF_INTERFACE_ID ..." ifconfig "$GIF_INTERFACE_ID" destroy || debug_out "Warning: failed to destroy" fi # ----------------------------------------------- # Create a new GIF interface. debug_out "Creating $GIF_INTERFACE_ID ..." # Exit if creation fails. if ! ifconfig "$GIF_INTERFACE_ID" create; then debug_out "ERROR: create failed"; exit 1; fi # ----------------------------------------------- # Configure the outer tunnel endpoints (local and remote). debug_out "Configuring outer tunnel..." # If the outer remote is IPv6, configure an IPv6 tunnel. if is_ipv6 "$OUTER_REMOTE"; then if ! ifconfig "$GIF_INTERFACE_ID" inet6 tunnel "$LOCAL_OUTER" "$OUTER_REMOTE"; then # On failure, debug_out error, destroy interface, and exit. debug_out "ERROR: failed tunnel $LOCAL_OUTER -> $OUTER_REMOTE" ifconfig "$GIF_INTERFACE_ID" destroy >/dev/null 2>&1 || true exit 1 fi else # Configure an IPv4 tunnel. if ! ifconfig "$GIF_INTERFACE_ID" tunnel "$LOCAL_OUTER" "$OUTER_REMOTE"; then # On failure, debug_out error, destroy interface, and exit. debug_out "ERROR: failed tunnel $LOCAL_OUTER -> $OUTER_REMOTE" ifconfig "$GIF_INTERFACE_ID" destroy >/dev/null 2>&1 || true exit 1 fi fi # ----------------------------------------------- # Configure inner IPv4 addresses for the tunnel if specified. # If either inner local or remote address is set. if [ -n "$INNER_LOCAL" ] || [ -n "$INNER_REMOTE" ]; then # If both inner local and remote are set (point-to-point). if [ -n "$INNER_LOCAL" ] && [ -n "$INNER_REMOTE" ]; then NETMASK="" # Convert CIDR to netmask if provided and valid. if printf "%s" "$INNER_REMOTE_NET" | grep -qE '^[0-9]+$'; then NETMASK="$(cidr2dotted "$INNER_REMOTE_NET")" fi # If a valid netmask exists, configure with netmask. if [ -n "$NETMASK" ]; then debug_out "Assigning inner IPv4 p2p: $INNER_LOCAL <-> $INNER_REMOTE netmask $NETMASK" ifconfig "$GIF_INTERFACE_ID" "$INNER_LOCAL" "$INNER_REMOTE" netmask "$NETMASK" || true else # Configure without netmask if none provided. debug_out "Assigning inner IPv4 p2p: $INNER_LOCAL <-> $INNER_REMOTE" ifconfig "$GIF_INTERFACE_ID" "$INNER_LOCAL" "$INNER_REMOTE" || true fi else # If only one address is provided, assign it as a single address. ONE="$( [ -n "$INNER_LOCAL" ] && echo "$INNER_LOCAL" || echo "$INNER_REMOTE" )" debug_out "Assigning single inner address $ONE" ifconfig "$GIF_INTERFACE_ID" "$ONE" || true fi fi # ----------------------------------------------- # Set the interface description, MTU # Set the interface description if available. [ -n "$DESC" ] && { echo "Setting description: $DESC"; ifconfig "$GIF_INTERFACE_ID" description "$DESC" || true; } # Set MTU if provided and valid. if [ -n "$IFACE_MTU" ]; then case "$IFACE_MTU" in # Skip if MTU contains non-numeric characters. *[!0-9]*) echo "Skipping invalid MTU: $IFACE_MTU" ;; # Set valid MTU. *) echo "Setting MTU: $IFACE_MTU"; ifconfig "$GIF_INTERFACE_ID" mtu "$IFACE_MTU" || true ;; esac fi # ----------------------------------------------- # Bring the GIF interface up. debug_out "Bringing $GIF_INTERFACE_ID up..." ifconfig "$GIF_INTERFACE_ID" up || true # ---- Restart pfSense WAN Interfaces --------------------------------------- # Restart all WAN interfaces to register the new gateway in pfSense. # Execute pfSense command to restart all WAN interfaces. /usr/local/sbin/pfSsh.php playback restartallwan # Exit with success status. exit 0