mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-01-23 02:14:26 +00:00
Merge faa5fc0e1a into e727620bd3
This commit is contained in:
commit
668ad7da73
5 changed files with 631 additions and 1 deletions
604
data/Dockerfiles/postfix/ipv6_smtp_controller.sh
Normal file
604
data/Dockerfiles/postfix/ipv6_smtp_controller.sh
Normal file
|
|
@ -0,0 +1,604 @@
|
|||
#!/usr/bin/env bash
|
||||
# ipv6_smtp_controller.sh
|
||||
# IPv6 SMTP sending eligibility controller for mailcow Postfix
|
||||
# This script determines whether IPv6 should be disabled for outgoing SMTP connections
|
||||
# based on configuration parameters, rDNS validation, and Spamhaus blocklist checks.
|
||||
|
||||
# Color codes for logging
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
GREEN='\033[0;32m'
|
||||
LIGHT_GREEN='\033[1;32m'
|
||||
LIGHT_RED='\033[1;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Logging function
|
||||
log_info() {
|
||||
echo -e "${YELLOW}[IPv6 SMTP Controller]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[IPv6 SMTP Controller]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[IPv6 SMTP Controller]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${LIGHT_RED}[IPv6 SMTP Controller]${NC} $1"
|
||||
}
|
||||
|
||||
# Extract IPv6 addresses from the system
|
||||
# Returns global scope IPv6 addresses suitable for SMTP sending
|
||||
# This function integrates with the existing IPv6 detection logic
|
||||
get_ipv6_addresses() {
|
||||
local ipv6_addresses=()
|
||||
|
||||
# Check if IPv6 is available at all
|
||||
if [[ ! -f /proc/net/if_inet6 ]] || grep -qs '^1' /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null; then
|
||||
log_info "IPv6 is not available or administratively disabled on the system"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Get all global IPv6 addresses (excluding link-local and loopback)
|
||||
local all_addresses
|
||||
all_addresses=$(ip -6 addr show scope global 2>/dev/null | grep 'inet6' | awk '{print $2}' | cut -d'/' -f1)
|
||||
|
||||
if [[ -z "$all_addresses" ]]; then
|
||||
log_info "No global IPv6 addresses found on the system"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Filter out any remaining non-global addresses
|
||||
while IFS= read -r ipv6_addr; do
|
||||
# Skip empty lines
|
||||
[[ -z "$ipv6_addr" ]] && continue
|
||||
|
||||
# Skip link-local (fe80::/10) and loopback (::1)
|
||||
if [[ "$ipv6_addr" =~ ^fe80: ]] || [[ "$ipv6_addr" =~ ^::1$ ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Skip ULA (Unique Local Addresses - fc00::/7) if desired
|
||||
# Uncomment the following line to skip ULA addresses:
|
||||
# [[ "$ipv6_addr" =~ ^f[cd] ]] && continue
|
||||
|
||||
ipv6_addresses+=("$ipv6_addr")
|
||||
done <<< "$all_addresses"
|
||||
|
||||
if [[ ${#ipv6_addresses[@]} -eq 0 ]]; then
|
||||
log_info "No suitable global IPv6 addresses found for SMTP sending"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Export addresses for use by calling functions
|
||||
printf '%s\n' "${ipv6_addresses[@]}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Check if IPv6 is supported and available for SMTP sending
|
||||
# This function reuses logic from the existing ipv6_controller.sh
|
||||
# Returns 0 if IPv6 is available, 1 otherwise
|
||||
check_ipv6_availability() {
|
||||
log_info "Checking IPv6 availability on the system..."
|
||||
|
||||
# Check 1: IPv6 kernel support and administrative status
|
||||
if [[ ! -f /proc/net/if_inet6 ]] || grep -qs '^1' /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null; then
|
||||
log_info "IPv6 is not available or administratively disabled on the system"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check 2: Global IPv6 addresses
|
||||
if ! ip -6 addr show scope global 2>/dev/null | grep -q 'inet6'; then
|
||||
log_info "No global IPv6 addresses found on the system"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check 3: IPv6 default route (optional but recommended)
|
||||
if ! ip -6 route show default 2>/dev/null | grep -qE '^default'; then
|
||||
log_warning "No default IPv6 route found - IPv6 connectivity may be limited"
|
||||
# Don't fail here - we may still have working IPv6 without a default route
|
||||
fi
|
||||
|
||||
log_success "IPv6 is available on the system"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Check rDNS resolution for IPv6 addresses
|
||||
# Validates that IPv6 addresses resolve back to MAILCOW_HOSTNAME
|
||||
# Returns 0 if rDNS is valid, 1 if invalid or check fails
|
||||
check_rdns_resolution() {
|
||||
local ipv6_address="$1"
|
||||
local expected_hostname="${MAILCOW_HOSTNAME}"
|
||||
|
||||
# Validate input parameters
|
||||
if [[ -z "$ipv6_address" ]]; then
|
||||
log_error "check_rdns_resolution: No IPv6 address provided"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -z "$expected_hostname" ]]; then
|
||||
log_error "check_rdns_resolution: MAILCOW_HOSTNAME is not set"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Checking rDNS for IPv6 address: $ipv6_address"
|
||||
log_info "Expected hostname: $expected_hostname"
|
||||
|
||||
# Perform reverse DNS lookup with timeout
|
||||
# Use host command with timeout to prevent hanging
|
||||
local rdns_result
|
||||
local timeout_seconds=5
|
||||
|
||||
# Try to resolve the IPv6 address to hostname
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
rdns_result=$(timeout "$timeout_seconds" host -W "$timeout_seconds" "$ipv6_address" 2>&1)
|
||||
else
|
||||
# Fallback if timeout command is not available
|
||||
rdns_result=$(host -W "$timeout_seconds" "$ipv6_address" 2>&1)
|
||||
fi
|
||||
|
||||
local host_exit_code=$?
|
||||
|
||||
# Check if the command succeeded
|
||||
if [[ $host_exit_code -ne 0 ]]; then
|
||||
if [[ $host_exit_code -eq 124 ]] || [[ "$rdns_result" == *"timed out"* ]]; then
|
||||
log_warning "rDNS lookup timed out for $ipv6_address"
|
||||
else
|
||||
log_warning "rDNS lookup failed for $ipv6_address: $rdns_result"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Parse the result to extract hostname
|
||||
# host command output format: "x.x.x.x.ip6.arpa domain name pointer hostname."
|
||||
local resolved_hostname
|
||||
resolved_hostname=$(echo "$rdns_result" | grep -i "domain name pointer" | awk '{print $NF}' | sed 's/\.$//')
|
||||
|
||||
if [[ -z "$resolved_hostname" ]]; then
|
||||
log_warning "No rDNS record found for $ipv6_address"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Resolved hostname: $resolved_hostname"
|
||||
|
||||
# Compare resolved hostname with expected hostname (case-insensitive)
|
||||
if [[ "${resolved_hostname,,}" == "${expected_hostname,,}" ]]; then
|
||||
log_success "rDNS validation passed: $resolved_hostname matches $expected_hostname"
|
||||
return 0
|
||||
else
|
||||
log_warning "rDNS validation failed: $resolved_hostname does not match $expected_hostname"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if IPv6 address is listed on Spamhaus blocklist
|
||||
# Queries zen.spamhaus.org with optional DQS key authentication
|
||||
# Returns 0 if NOT listed (clean), 1 if listed or check fails
|
||||
check_spamhaus_listing() {
|
||||
local ipv6_address="$1"
|
||||
|
||||
# Validate input parameter
|
||||
if [[ -z "$ipv6_address" ]]; then
|
||||
log_error "check_spamhaus_listing: No IPv6 address provided"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Checking Spamhaus blocklist for IPv6 address: $ipv6_address"
|
||||
|
||||
# Convert IPv6 address to reverse DNS format for Spamhaus query
|
||||
# IPv6 addresses need to be converted to nibble format (reverse hex digits with dots)
|
||||
# Example: 2001:db8::1 becomes 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2
|
||||
|
||||
# Expand IPv6 address to full format first
|
||||
local expanded_ipv6
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
expanded_ipv6=$(python3 -c "import ipaddress; print(ipaddress.IPv6Address('$ipv6_address').exploded)" 2>/dev/null)
|
||||
else
|
||||
log_warning "Python not available for IPv6 address expansion, skipping Spamhaus check"
|
||||
return 0 # Treat as not listed if we can't check
|
||||
fi
|
||||
|
||||
if [[ -z "$expanded_ipv6" ]]; then
|
||||
log_warning "Failed to expand IPv6 address: $ipv6_address"
|
||||
return 0 # Treat as not listed if expansion fails
|
||||
fi
|
||||
|
||||
# Convert expanded IPv6 to nibble format (reverse hex digits)
|
||||
# Remove colons and reverse the string, then add dots between each character
|
||||
local nibble_format
|
||||
nibble_format=$(echo "$expanded_ipv6" | tr -d ':' | rev | sed 's/./&./g' | sed 's/\.$//')
|
||||
|
||||
if [[ -z "$nibble_format" ]]; then
|
||||
log_warning "Failed to convert IPv6 address to nibble format"
|
||||
return 0 # Treat as not listed if conversion fails
|
||||
fi
|
||||
|
||||
# Determine which Spamhaus zone to query
|
||||
local spamhaus_zone
|
||||
if [[ -n "${SPAMHAUS_DQS_KEY}" ]]; then
|
||||
# Use authenticated DQS endpoint
|
||||
spamhaus_zone="${nibble_format}.${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net"
|
||||
log_info "Using authenticated Spamhaus DQS query"
|
||||
else
|
||||
# Use public endpoint
|
||||
spamhaus_zone="${nibble_format}.zen.spamhaus.org"
|
||||
log_info "Using public Spamhaus query (unauthenticated)"
|
||||
fi
|
||||
|
||||
# Perform DNS query with timeout and retry logic
|
||||
local timeout_seconds=5
|
||||
local max_retries=2
|
||||
local retry_count=0
|
||||
local query_result
|
||||
local query_exit_code
|
||||
|
||||
while [[ $retry_count -lt $max_retries ]]; do
|
||||
log_info "Querying Spamhaus (attempt $((retry_count + 1))/$max_retries): $spamhaus_zone"
|
||||
|
||||
# Use host command to query the blocklist
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
query_result=$(timeout "$timeout_seconds" host -W "$timeout_seconds" -t A "$spamhaus_zone" 2>&1)
|
||||
else
|
||||
query_result=$(host -W "$timeout_seconds" -t A "$spamhaus_zone" 2>&1)
|
||||
fi
|
||||
|
||||
query_exit_code=$?
|
||||
|
||||
# Check the result
|
||||
if [[ $query_exit_code -eq 0 ]]; then
|
||||
# Query succeeded - check if address is listed
|
||||
if echo "$query_result" | grep -q "has address"; then
|
||||
# Extract the return code (127.0.0.x format)
|
||||
local return_code
|
||||
return_code=$(echo "$query_result" | grep "has address" | awk '{print $NF}' | head -n1)
|
||||
|
||||
log_warning "IPv6 address IS LISTED on Spamhaus: $ipv6_address (return code: $return_code)"
|
||||
return 1 # Listed - should disable IPv6
|
||||
else
|
||||
log_success "IPv6 address is NOT listed on Spamhaus: $ipv6_address"
|
||||
return 0 # Not listed - OK to use IPv6
|
||||
fi
|
||||
elif [[ $query_exit_code -eq 1 ]] || echo "$query_result" | grep -qi "NXDOMAIN\|not found"; then
|
||||
# NXDOMAIN means not listed - this is good
|
||||
log_success "IPv6 address is NOT listed on Spamhaus: $ipv6_address"
|
||||
return 0 # Not listed - OK to use IPv6
|
||||
elif [[ $query_exit_code -eq 124 ]] || echo "$query_result" | grep -qi "timed out"; then
|
||||
# Timeout - retry
|
||||
log_warning "Spamhaus query timed out (attempt $((retry_count + 1))/$max_retries)"
|
||||
retry_count=$((retry_count + 1))
|
||||
|
||||
if [[ $retry_count -lt $max_retries ]]; then
|
||||
sleep 2 # Wait before retry
|
||||
fi
|
||||
else
|
||||
# Other error - could be rate limiting or API issue
|
||||
log_warning "Spamhaus query failed: $query_result"
|
||||
retry_count=$((retry_count + 1))
|
||||
|
||||
if [[ $retry_count -lt $max_retries ]]; then
|
||||
sleep 2 # Wait before retry
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# If we exhausted retries, treat as check failure
|
||||
# Default to allowing IPv6 (fail open) to avoid false positives
|
||||
log_warning "Spamhaus check failed after $max_retries attempts, treating as NOT listed (fail-open)"
|
||||
return 0 # Treat as not listed if check fails
|
||||
}
|
||||
|
||||
# Main function to check IPv6 SMTP sending eligibility
|
||||
# Sets DISABLE_IPV6_SMTP_SENDING environment variable based on checks
|
||||
# Implements comprehensive decision logic with fallback mechanisms
|
||||
check_ipv6_smtp_sending_eligibility() {
|
||||
echo "========================================================================"
|
||||
log_info "Starting IPv6 SMTP sending eligibility check..."
|
||||
log_info "Timestamp: $(date '+%Y-%m-%d %H:%M:%S %Z')"
|
||||
echo "========================================================================"
|
||||
|
||||
# Initialize decision tracking variables
|
||||
local should_disable_ipv6=false
|
||||
local disable_reason=""
|
||||
local check_results=()
|
||||
|
||||
# Track individual check outcomes for comprehensive logging
|
||||
local config_check_result="not_checked"
|
||||
local ipv6_availability_result="not_checked"
|
||||
local ipv6_addresses_result="not_checked"
|
||||
local rdns_check_result="not_checked"
|
||||
local spamhaus_check_result="not_checked"
|
||||
|
||||
# ============================================================================
|
||||
# Check 1: Read configuration parameter (highest priority)
|
||||
# ============================================================================
|
||||
log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
log_info "CHECK 1: Configuration Parameter"
|
||||
log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
if [[ -n "${DISABLE_IPV6_SMTP_SENDING}" ]]; then
|
||||
log_info "DISABLE_IPV6_SMTP_SENDING is set to: '${DISABLE_IPV6_SMTP_SENDING}'"
|
||||
|
||||
if [[ "${DISABLE_IPV6_SMTP_SENDING}" =~ ^([yY][eE][sS]|[yY]|[tT][rR][uU][eE]|1)$ ]]; then
|
||||
log_warning "IPv6 SMTP sending is EXPLICITLY DISABLED via configuration parameter"
|
||||
should_disable_ipv6=true
|
||||
disable_reason="Explicitly disabled in configuration (DISABLE_IPV6_SMTP_SENDING=${DISABLE_IPV6_SMTP_SENDING})"
|
||||
config_check_result="disabled_by_config"
|
||||
check_results+=("CONFIG: Explicitly disabled")
|
||||
|
||||
# Export and return immediately - configuration override takes precedence
|
||||
export DISABLE_IPV6_SMTP_SENDING="true"
|
||||
|
||||
echo "========================================================================"
|
||||
log_warning "FINAL DECISION: IPv6 SMTP sending DISABLED"
|
||||
log_warning "Reason: ${disable_reason}"
|
||||
log_info "All checks bypassed due to explicit configuration"
|
||||
echo "========================================================================"
|
||||
return 0
|
||||
|
||||
elif [[ "${DISABLE_IPV6_SMTP_SENDING}" =~ ^([nN][oO]|[nN]|[fF][aA][lL][sS][eE]|0)$ ]]; then
|
||||
log_success "Configuration parameter explicitly ALLOWS IPv6 SMTP sending"
|
||||
config_check_result="enabled_by_config"
|
||||
check_results+=("CONFIG: Explicitly enabled, proceeding with validation checks")
|
||||
else
|
||||
log_warning "Invalid DISABLE_IPV6_SMTP_SENDING value: '${DISABLE_IPV6_SMTP_SENDING}'"
|
||||
log_warning "Valid values: yes/y/true/1 (disable) or no/n/false/0 (enable)"
|
||||
log_info "Treating invalid value as 'no' (IPv6 enabled) and continuing checks"
|
||||
config_check_result="invalid_value_treated_as_no"
|
||||
check_results+=("CONFIG: Invalid value, defaulting to enabled")
|
||||
fi
|
||||
else
|
||||
log_info "DISABLE_IPV6_SMTP_SENDING not set in configuration"
|
||||
log_info "Defaulting to 'no' (IPv6 enabled) - will perform validation checks"
|
||||
config_check_result="not_set_default_enabled"
|
||||
check_results+=("CONFIG: Not set, defaulting to enabled")
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# Check 2: Verify IPv6 is available on the system
|
||||
# ============================================================================
|
||||
log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
log_info "CHECK 2: IPv6 System Availability"
|
||||
log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
if ! check_ipv6_availability; then
|
||||
log_warning "IPv6 is NOT available on the system"
|
||||
should_disable_ipv6=true
|
||||
disable_reason="IPv6 not available on system (no kernel support or administratively disabled)"
|
||||
ipv6_availability_result="not_available"
|
||||
check_results+=("AVAILABILITY: IPv6 not available on system")
|
||||
|
||||
# Export and return - no point in further checks
|
||||
export DISABLE_IPV6_SMTP_SENDING="true"
|
||||
|
||||
echo "========================================================================"
|
||||
log_warning "FINAL DECISION: IPv6 SMTP sending DISABLED"
|
||||
log_warning "Reason: ${disable_reason}"
|
||||
log_info "Remaining checks skipped (IPv6 not available)"
|
||||
echo "========================================================================"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_success "IPv6 is available on the system"
|
||||
ipv6_availability_result="available"
|
||||
check_results+=("AVAILABILITY: IPv6 available")
|
||||
|
||||
# ============================================================================
|
||||
# Check 3: Extract IPv6 addresses from the system
|
||||
# ============================================================================
|
||||
log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
log_info "CHECK 3: IPv6 Address Detection"
|
||||
log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
local ipv6_addresses
|
||||
ipv6_addresses=$(get_ipv6_addresses)
|
||||
local get_addresses_result=$?
|
||||
|
||||
if [[ $get_addresses_result -ne 0 ]] || [[ -z "$ipv6_addresses" ]]; then
|
||||
log_warning "No suitable IPv6 addresses found for SMTP sending"
|
||||
should_disable_ipv6=true
|
||||
disable_reason="No global IPv6 addresses found on system"
|
||||
ipv6_addresses_result="no_addresses_found"
|
||||
check_results+=("ADDRESSES: No global IPv6 addresses found")
|
||||
|
||||
# Export and return - no addresses to validate
|
||||
export DISABLE_IPV6_SMTP_SENDING="true"
|
||||
|
||||
echo "========================================================================"
|
||||
log_warning "FINAL DECISION: IPv6 SMTP sending DISABLED"
|
||||
log_warning "Reason: ${disable_reason}"
|
||||
log_info "Remaining checks skipped (no IPv6 addresses)"
|
||||
echo "========================================================================"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Count and display found addresses
|
||||
local ipv6_address_count
|
||||
ipv6_address_count=$(echo "$ipv6_addresses" | wc -l)
|
||||
log_success "Found ${ipv6_address_count} global IPv6 address(es) for validation:"
|
||||
|
||||
while IFS= read -r ipv6_addr; do
|
||||
log_info " ➜ $ipv6_addr"
|
||||
done <<< "$ipv6_addresses"
|
||||
|
||||
ipv6_addresses_result="found_${ipv6_address_count}_addresses"
|
||||
check_results+=("ADDRESSES: Found ${ipv6_address_count} global IPv6 address(es)")
|
||||
|
||||
# ============================================================================
|
||||
# Check 4: Validate rDNS for IPv6 addresses
|
||||
# ============================================================================
|
||||
log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
log_info "CHECK 4: Reverse DNS (rDNS) Validation"
|
||||
log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
local rdns_check_passed=false
|
||||
local rdns_check_attempted=false
|
||||
local rdns_passed_count=0
|
||||
local rdns_failed_count=0
|
||||
|
||||
# Check rDNS for each IPv6 address
|
||||
while IFS= read -r ipv6_addr; do
|
||||
[[ -z "$ipv6_addr" ]] && continue
|
||||
|
||||
rdns_check_attempted=true
|
||||
|
||||
if check_rdns_resolution "$ipv6_addr"; then
|
||||
rdns_check_passed=true
|
||||
rdns_passed_count=$((rdns_passed_count + 1))
|
||||
log_success "✓ rDNS validation PASSED for: $ipv6_addr"
|
||||
else
|
||||
rdns_failed_count=$((rdns_failed_count + 1))
|
||||
log_warning "✗ rDNS validation FAILED for: $ipv6_addr"
|
||||
fi
|
||||
done <<< "$ipv6_addresses"
|
||||
|
||||
# Evaluate rDNS check results
|
||||
if [[ "$rdns_check_attempted" == "true" ]]; then
|
||||
if [[ "$rdns_check_passed" == "false" ]]; then
|
||||
log_error "rDNS validation FAILED for ALL ${rdns_failed_count} IPv6 address(es)"
|
||||
log_error "This indicates improper reverse DNS configuration"
|
||||
should_disable_ipv6=true
|
||||
disable_reason="rDNS validation failed for all IPv6 addresses (${rdns_failed_count} failed)"
|
||||
rdns_check_result="all_failed"
|
||||
check_results+=("rDNS: FAILED for all ${rdns_failed_count} address(es)")
|
||||
else
|
||||
log_success "rDNS validation PASSED for ${rdns_passed_count} of ${ipv6_address_count} IPv6 address(es)"
|
||||
if [[ $rdns_failed_count -gt 0 ]]; then
|
||||
log_info "Note: ${rdns_failed_count} address(es) failed rDNS, but at least one passed"
|
||||
fi
|
||||
rdns_check_result="passed_${rdns_passed_count}_of_${ipv6_address_count}"
|
||||
check_results+=("rDNS: PASSED for ${rdns_passed_count}/${ipv6_address_count} address(es)")
|
||||
fi
|
||||
else
|
||||
log_warning "rDNS check was not attempted (no addresses to check)"
|
||||
rdns_check_result="not_attempted"
|
||||
check_results+=("rDNS: Not attempted")
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# Check 5: Query Spamhaus blocklist for IPv6 addresses
|
||||
# ============================================================================
|
||||
log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
log_info "CHECK 5: Spamhaus Blocklist Validation"
|
||||
log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
local spamhaus_check_passed=true
|
||||
local spamhaus_check_attempted=false
|
||||
local spamhaus_clean_count=0
|
||||
local spamhaus_listed_count=0
|
||||
|
||||
# Check Spamhaus for each IPv6 address
|
||||
while IFS= read -r ipv6_addr; do
|
||||
[[ -z "$ipv6_addr" ]] && continue
|
||||
|
||||
spamhaus_check_attempted=true
|
||||
|
||||
# check_spamhaus_listing returns 0 if NOT listed (clean), 1 if listed
|
||||
if check_spamhaus_listing "$ipv6_addr"; then
|
||||
spamhaus_clean_count=$((spamhaus_clean_count + 1))
|
||||
log_success "✓ Spamhaus check PASSED (not listed): $ipv6_addr"
|
||||
else
|
||||
spamhaus_listed_count=$((spamhaus_listed_count + 1))
|
||||
log_error "✗ Spamhaus check FAILED (LISTED): $ipv6_addr"
|
||||
spamhaus_check_passed=false
|
||||
# Don't break - check all addresses for complete logging
|
||||
fi
|
||||
done <<< "$ipv6_addresses"
|
||||
|
||||
# Evaluate Spamhaus check results
|
||||
if [[ "$spamhaus_check_attempted" == "true" ]]; then
|
||||
if [[ "$spamhaus_check_passed" == "false" ]]; then
|
||||
log_error "Spamhaus blocklist check FAILED: ${spamhaus_listed_count} address(es) are LISTED"
|
||||
log_error "This indicates the IPv6 address(es) have poor reputation"
|
||||
should_disable_ipv6=true
|
||||
|
||||
# Update disable reason if not already set by rDNS
|
||||
if [[ -z "$disable_reason" ]]; then
|
||||
disable_reason="IPv6 address(es) listed on Spamhaus blocklist (${spamhaus_listed_count} listed)"
|
||||
else
|
||||
disable_reason="${disable_reason}; IPv6 address(es) listed on Spamhaus (${spamhaus_listed_count} listed)"
|
||||
fi
|
||||
|
||||
spamhaus_check_result="listed_${spamhaus_listed_count}_of_${ipv6_address_count}"
|
||||
check_results+=("SPAMHAUS: FAILED - ${spamhaus_listed_count}/${ipv6_address_count} address(es) LISTED")
|
||||
else
|
||||
log_success "Spamhaus blocklist check PASSED for all ${spamhaus_clean_count} IPv6 address(es)"
|
||||
spamhaus_check_result="all_clean"
|
||||
check_results+=("SPAMHAUS: PASSED - all ${spamhaus_clean_count} address(es) clean")
|
||||
fi
|
||||
else
|
||||
log_warning "Spamhaus check was not attempted (no addresses to check)"
|
||||
spamhaus_check_result="not_attempted"
|
||||
check_results+=("SPAMHAUS: Not attempted")
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# Final Decision Logic with Comprehensive Logging
|
||||
# ============================================================================
|
||||
echo "========================================================================"
|
||||
log_info "DECISION SUMMARY"
|
||||
echo "========================================================================"
|
||||
|
||||
# Log all check results
|
||||
log_info "Check Results:"
|
||||
for result in "${check_results[@]}"; do
|
||||
log_info " • $result"
|
||||
done
|
||||
|
||||
echo "------------------------------------------------------------------------"
|
||||
|
||||
# Make final decision and export result
|
||||
if [[ "$should_disable_ipv6" == "true" ]]; then
|
||||
log_error "FINAL DECISION: IPv6 SMTP sending will be DISABLED"
|
||||
log_error "Reason: ${disable_reason}"
|
||||
export DISABLE_IPV6_SMTP_SENDING="true"
|
||||
|
||||
# Log troubleshooting information
|
||||
echo "------------------------------------------------------------------------"
|
||||
log_info "Troubleshooting Information:"
|
||||
log_info " • Configuration: ${config_check_result}"
|
||||
log_info " • IPv6 Availability: ${ipv6_availability_result}"
|
||||
log_info " • IPv6 Addresses: ${ipv6_addresses_result}"
|
||||
log_info " • rDNS Validation: ${rdns_check_result}"
|
||||
log_info " • Spamhaus Check: ${spamhaus_check_result}"
|
||||
|
||||
if [[ "$rdns_check_result" == "all_failed" ]]; then
|
||||
echo "------------------------------------------------------------------------"
|
||||
log_info "rDNS Fix Recommendations:"
|
||||
log_info " 1. Ensure PTR records are configured for all IPv6 addresses"
|
||||
log_info " 2. Verify PTR records resolve to: ${MAILCOW_HOSTNAME}"
|
||||
log_info " 3. Check with your hosting provider or DNS administrator"
|
||||
log_info " 4. Test with: host <ipv6_address>"
|
||||
fi
|
||||
|
||||
if [[ "$spamhaus_check_result" =~ ^listed_ ]]; then
|
||||
echo "------------------------------------------------------------------------"
|
||||
log_info "Spamhaus Listing Recommendations:"
|
||||
log_info " 1. Check listing details at: https://check.spamhaus.org/"
|
||||
log_info " 2. Follow Spamhaus delisting procedures if incorrectly listed"
|
||||
log_info " 3. Consider using different IPv6 addresses"
|
||||
log_info " 4. Review email sending practices and security"
|
||||
fi
|
||||
|
||||
else
|
||||
log_success "FINAL DECISION: IPv6 SMTP sending will be ENABLED"
|
||||
log_success "All validation checks passed successfully"
|
||||
export DISABLE_IPV6_SMTP_SENDING="false"
|
||||
|
||||
# Log success details
|
||||
echo "------------------------------------------------------------------------"
|
||||
log_info "Validation Summary:"
|
||||
log_info " • Configuration: ${config_check_result}"
|
||||
log_info " • IPv6 Availability: ${ipv6_availability_result}"
|
||||
log_info " • IPv6 Addresses: ${ipv6_addresses_result}"
|
||||
log_info " • rDNS Validation: ${rdns_check_result}"
|
||||
log_info " • Spamhaus Check: ${spamhaus_check_result}"
|
||||
fi
|
||||
|
||||
echo "========================================================================"
|
||||
log_info "IPv6 SMTP eligibility check completed at: $(date '+%Y-%m-%d %H:%M:%S %Z')"
|
||||
echo "========================================================================"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
|
@ -487,6 +487,19 @@ sed -i '/\$myhostname/! { /myhostname/d }' /opt/postfix/conf/extra.cf
|
|||
echo -e "myhostname = ${MAILCOW_HOSTNAME}\n$(cat /opt/postfix/conf/extra.cf)" > /opt/postfix/conf/extra.cf
|
||||
cat /opt/postfix/conf/extra.cf >> /opt/postfix/conf/main.cf
|
||||
|
||||
# Check IPv6 SMTP sending eligibility
|
||||
source /usr/local/bin/ipv6_smtp_controller.sh
|
||||
check_ipv6_smtp_sending_eligibility
|
||||
|
||||
# Process tags in master.cf based on IPv6 eligibility
|
||||
if [[ "${DISABLE_IPV6_SMTP_SENDING}" == "true" ]]; then
|
||||
# Convert magic tags to actual IPv4-only configuration
|
||||
sed -i 's/# Override: IPv4 Only #/ -o inet_protocols=ipv4/' /opt/postfix/conf/master.cf
|
||||
else
|
||||
# Remove magic tags to allow IPv6
|
||||
sed -i '/# Override: IPv4 Only #/d' /opt/postfix/conf/master.cf
|
||||
fi
|
||||
|
||||
if [ ! -f /opt/postfix/conf/custom_transport.pcre ]; then
|
||||
echo "Creating dummy custom_transport.pcre"
|
||||
touch /opt/postfix/conf/custom_transport.pcre
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# inter-mx with postscreen on 25/tcp
|
||||
smtp inet n - n - 1 postscreen
|
||||
# Override: IPv4 Only #
|
||||
|
||||
10025 inet n - n - 1 postscreen
|
||||
-o postscreen_upstream_proxy_protocol=haproxy
|
||||
-o syslog_name=haproxy
|
||||
|
|
@ -77,11 +79,13 @@ smtp_enforced_tls unix - - n - - smtp
|
|||
-o smtp_tls_security_level=encrypt
|
||||
-o syslog_name=enforced-tls-smtp
|
||||
-o smtp_delivery_status_filter=pcre:/opt/postfix/conf/smtp_dsn_filter
|
||||
# Override: IPv4 Only #
|
||||
|
||||
# smtp connector used, when a transport map matched
|
||||
# this helps to have different sasl maps than we have with sender dependent transport maps
|
||||
smtp_via_transport_maps unix - - n - - smtp
|
||||
-o smtp_sasl_password_maps=proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf
|
||||
# Override: IPv4 Only #
|
||||
|
||||
tlsproxy unix - - n - 0 tlsproxy
|
||||
dnsblog unix - - n - 0 dnsblog
|
||||
|
|
@ -144,3 +148,6 @@ watchdog_discard unix - - n - - discard
|
|||
-o syslog_facility=local7
|
||||
-o syslog_name=watchdog
|
||||
# end watchdog-specific
|
||||
|
||||
# DO NOT EDIT ANYTHING BELOW #
|
||||
# Overrides #
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ services:
|
|||
- redis
|
||||
|
||||
clamd-mailcow:
|
||||
image: ghcr.io/mailcow/clamd:1.71
|
||||
image: mailcow/clamd:1.66
|
||||
restart: always
|
||||
depends_on:
|
||||
unbound-mailcow:
|
||||
|
|
|
|||
|
|
@ -436,6 +436,12 @@ SPAMHAUS_DQS_KEY=
|
|||
# A COMPLETE DOCKER STACK REBUILD (compose down && compose up -d) IS NEEDED TO APPLY THIS.
|
||||
ENABLE_IPV6=${IPV6_BOOL}
|
||||
|
||||
# Disable IPv6 for outgoing SMTP connections - y/n
|
||||
# When set to 'y', forces Postfix to use IPv4 only for outgoing mail delivery
|
||||
# This can help prevent delivery issues when IPv6 configuration is problematic
|
||||
# Defaults to 'n' (IPv6 enabled for SMTP sending)
|
||||
DISABLE_IPV6_SMTP_SENDING=n
|
||||
|
||||
# Prevent netfilter from setting an iptables/nftables rule to isolate the mailcow docker network - y/n
|
||||
# CAUTION: Disabling this may expose container ports to other neighbors on the same subnet, even if the ports are bound to localhost
|
||||
DISABLE_NETFILTER_ISOLATION_RULE=n
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue