# Complete Enhancement Package - Major Feature Update Comprehensive enhancement package for docker-ddns-server including security features, modern authentication, UI/UX improvements, and production-ready deployment features. ## 🔒 Security & Authentication ### IP Blocking System - Implemented automatic IP blocking after 3 failed authentication attempts within 72 hours - Added 7-day block duration with automatic expiration - Created `blocked_ips` database table for tracking blocked addresses - Added automatic cleanup of expired blocks - Implemented manual IP unblock capability via security dashboard ### Failed Authentication Logging - Added comprehensive failed authentication logging system - Created `failed_auths` database table storing IP, timestamp, username, and password - Implemented threat intelligence features for password pattern analysis - Added automatic cleanup of old authentication records - Logs intentionally include passwords for single-user security analysis ### Session-Based Authentication - Replaced HTTP Basic Auth with modern session-based authentication for admin panel - Integrated gorilla/sessions library for secure session management - Added configurable session secrets via `DDNS_SESSION_SECRET` environment variable - Implemented "Remember Me" functionality with 30-day session duration - Added proper session destruction on logout - Session cookies configured with HttpOnly, Secure, and SameSite attributes - Maintained HTTP Basic Auth for DynDNS API endpoints (device compatibility) ### HTTPS Enforcement - Added intelligent HTTPS detection via multiple header checks - Implemented automatic HTTPS redirect for admin panel when available - Graceful HTTP fallback when HTTPS unavailable - Supports reverse proxy configurations (nginx, Caddy, Traefik) - Detects SSL via X-Forwarded-Proto, X-Forwarded-Ssl, X-Url-Scheme headers - API endpoints remain HTTP-compatible for device support ## 🎨 UI/UX Enhancements ### Authentication UI - Created modern login page with gradient background and clean design - Added HTTPS security indicator (✓ green / ⚠ yellow) - Implemented auto-focus on username field - Added clear error messages for failed login attempts - Created logout confirmation page with redirect options - Removed browser authentication dialog popups ### Navigation & Layout - Changed admin panel URL from `/admin` to `/@` for uniqueness - Updated navigation with unicode icons (🏠 Dashboard, 🔒 Security, ⏏️ Logout) - Added tooltips to all navigation icons - Implemented sticky header that remains visible on scroll - Enhanced responsive design for mobile/tablet access ### Logo Support - Added automatic logo detection and display - Supports PNG, WebP, and SVG formats - Checks `/static/icons/` for logo files - Graceful fallback to text title if no logo found - Maintains aspect ratio and responsive sizing ### Security Dashboard - Created comprehensive security overview page at `/@/security` - Added statistics cards showing active blocks, failed attempts, and total blocks - Implemented recent failed attempts table with sortable columns - Added password reveal/hide functionality with confirmation prompts - Created detailed blocked IPs management page with unblock capability - Created detailed failed authentication logs page with full history - Added visual indicators for security status ## 📊 Data Management ### Data Consistency & Normalization - Implemented automatic lowercase conversion for all usernames and hostnames - Prevents case-sensitivity issues in DNS lookups and authentication - Ensures consistent data storage and retrieval - Handles mixed-case legacy data gracefully ### Automatic Migration - Added on-the-fly migration system for legacy uppercase entries - Migration triggers automatically on first `/@/hosts` page visit - Handles hostname conflicts by appending sequential numbers - Provides detailed migration report in UI showing all changes - Non-destructive migration preserves all host data - One-time execution with persistent migration status tracking ### Validation Updates - Reduced minimum hostname length to 1 character (allows single-letter subdomains) - Reduced minimum username length to 1 character - Reduced minimum password length to 6 characters - Maintained security while improving flexibility ### Username Uniqueness - Removed uniqueness constraint on usernames - Allows multiple hosts to share the same username - Supports different passwords for same username across hosts - Enables more flexible credential management strategies ## 🛡️ Middleware & Request Handling ### IP Blocker Middleware - Created IPBlockerMiddleware to check requests against blocked IPs - Automatic redirect to 127.0.0.1 for blocked addresses - Lightweight performance impact with database lookup - Positioned early in middleware chain for efficiency ### Session Authentication Middleware - Created SessionAuthMiddleware for admin panel protection - Skips authentication check for /login and /logout routes - Redirects unauthenticated users to login page - Validates session integrity on every request - Compatible with reverse proxy configurations ### HTTPS Redirect Middleware - Created HTTPSRedirectMiddleware for admin panel security - Intelligent detection of HTTPS availability - Skips redirect for API endpoints - Handles X-Forwarded-* headers from reverse proxies - Graceful operation when HTTPS unavailable ## 🗄️ Database & Models ### New Tables - Added `failed_auths` table for authentication logging - Added `blocked_ips` table for IP block tracking - Proper foreign key relationships and indexes - Automatic timestamps on all records ### Cleanup Functions - Implemented automatic cleanup of expired IP blocks - Implemented automatic cleanup of old authentication logs - Configurable retention periods - Background cleanup execution ## 🔧 Technical Improvements ### Dependencies - Added `github.com/gorilla/sessions@v1.2.2` for session management - Updated go.mod with proper version constraints - Maintained compatibility with existing dependencies ### Handler Architecture - Separated security logic into dedicated handler files - Created `security.go` for blocking logic and logging - Created `security_dashboard.go` for UI handlers - Created `auth.go` for login/logout and session management - Created `session.go` for session store implementation - Improved code organization and maintainability ### Main Application - Updated routing to support session-based authentication - Added session initialization on startup - Configured route groups for admin panel and API - Middleware ordering optimized for performance and security ## 🐳 Docker & CI/CD ### Multi-Platform Builds & Automated Releases - Created GitHub Actions workflow (`BuildEmAll.yml`) for automated Docker builds - Supports linux/amd64, linux/386, linux/arm/v7, and linux/arm64 platforms - Automatic builds on push to master with dyndns/ directory changes - Intelligent version tagging system: - Extracts version from commit message (e.g., "v1.2.3 Feature description") - Auto-increments patch version from latest git tag - Falls back to date-based versioning (vYY.MM.DD-HHMM) if no tags exist - Tags images with both `:latest` and semantic version tags (`:vX.Y.Z`) - Automatic GitHub release creation with each build - Release includes Docker image reference and commit message as notes - Publishes to Docker Hub (w3kllc/ddns) - Cross-platform compatibility for ARM devices (Raspberry Pi, etc.) - Workflow can be triggered manually via GitHub Actions UI ### Deployment - Enhanced docker-compose.yml example with all new features - Added documentation for environment variable configuration - Included reverse proxy configuration examples - Added security best practices for production deployment - Semantic versioning with automatic release management ## 📝 Documentation ### README Enhancements - Added comprehensive Security Features section - Added Environment Variables reference with descriptions - Added Admin Panel Access documentation - Added Data Consistency & Migration guide - Added API Endpoints documentation - Added UI/UX Enhancements overview - Added Reverse Proxy Configuration examples - Added Docker Configuration best practices - Added CI/CD & Multi-Platform Support details with versioning strategy - Added Semantic Versioning documentation - Added GitHub Release automation details - Added Security Best Practices recommendations - Added Threat Intelligence rationale - Added Migration Guide from original project - Added Troubleshooting section - Added API Reference documentation - Added Roadmap for future features - Updated Credits section - Added Support and Community links ## 🔄 Backward Compatibility ### Maintained Features - DynDNS API endpoints remain unchanged (/update, /nic/update, etc.) - HTTP Basic Auth still supported for API (device compatibility) - Existing host configurations continue working without changes - Database schema additions are non-breaking - All original functionality preserved ### Breaking Changes - Admin panel URL changed from `/admin` to `/@` (intentional, more unique) - Admin authentication method changed (sessions vs basic auth) - Requires `DDNS_SESSION_SECRET` environment variable for session security ## ⚡ Performance Considerations - IP blocker checks are optimized with database indexing - Session validation cached in memory - Automatic cleanup runs asynchronously - Minimal overhead on API endpoint performance - Efficient middleware ordering ## 🎯 Testing Considerations Recommended testing areas: - Login/logout flow with and without HTTPS - IP blocking after 3 failed attempts - Session persistence with remember me - API endpoint authentication (device compatibility) - HTTPS redirect with reverse proxy headers - Password reveal/hide in security dashboard - Hostname migration for legacy uppercase entries - Multi-platform Docker image functionality --- **Total Changes:** - **21 files modified** - **20 new files created** - **~2000+ lines of code added** - **100+ hours of development time** **Compatibility:** - ✅ Backward compatible for DynDNS API - ⚠️ Admin panel URL changed (bookmark update needed) - ✅ All existing hosts continue working - ✅ Database schema additions are additive **Credits:** - Original project: dprandzioch/docker-ddns - Web UI Fork: benjaminbear/docker-ddns-server - Enhanced fork: w3K-one/docker-ddns-server - Major enhancements and security features added This represents a significant enhancement to the original project while maintaining the core DynDNS functionality and adding modern security, authentication, and user experience improvements suitable for production deployment.
236 lines
5.9 KiB
Go
236 lines
5.9 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/labstack/gommon/log"
|
|
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/w3K-one/docker-ddns-server/dyndns/model"
|
|
"github.com/go-playground/validator/v10"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/tg123/go-htpasswd"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type Handler struct {
|
|
DB *gorm.DB
|
|
AuthAdmin bool
|
|
Config Envs
|
|
Title string
|
|
DisableAdminAuth bool
|
|
LastClearedLogs time.Time
|
|
ClearInterval uint64
|
|
AllowWildcard bool
|
|
LogoutUrl string
|
|
LogoPath string
|
|
SessionStore *SessionStore
|
|
PoweredBy string
|
|
PoweredByUrl string
|
|
}
|
|
|
|
type Envs struct {
|
|
AdminLogin string
|
|
Domains []string
|
|
}
|
|
|
|
type CustomValidator struct {
|
|
Validator *validator.Validate
|
|
}
|
|
|
|
// Validate implements the Validator.
|
|
func (cv *CustomValidator) Validate(i interface{}) error {
|
|
return cv.Validator.Struct(i)
|
|
}
|
|
|
|
type Error struct {
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// Authenticate is the method the website admin user and the host update user have to authenticate against.
|
|
// To gather admin rights the username password combination must match with the credentials given by the env var.
|
|
func (h *Handler) AuthenticateUpdate(username, password string, c echo.Context) (bool, error) {
|
|
h.CheckClearInterval()
|
|
reqParameter := strings.ToLower(c.QueryParam("hostname"))
|
|
reqArr := strings.SplitN(reqParameter, ".", 2)
|
|
if len(reqArr) != 2 {
|
|
log.Error("Error: Something wrong with the hostname parameter")
|
|
return false, nil
|
|
}
|
|
|
|
lowerUsername := strings.ToLower(username)
|
|
host := &model.Host{}
|
|
if err := h.DB.Where(&model.Host{UserName: lowerUsername, Password: password, Hostname: reqArr[0], Domain: reqArr[1]}).First(host).Error; err != nil {
|
|
log.Error("Error: ", err)
|
|
return false, nil
|
|
}
|
|
if host.ID == 0 {
|
|
log.Error("hostname or user user credentials unknown")
|
|
return false, nil
|
|
}
|
|
c.Set("updateHost", host)
|
|
|
|
return true, nil
|
|
}
|
|
func (h *Handler) AuthenticateAdmin(username, password string, c echo.Context) (bool, error) {
|
|
h.AuthAdmin = false
|
|
ok, err := h.authByEnv(username, password)
|
|
if err != nil {
|
|
log.Error("Error:", err)
|
|
return false, nil
|
|
}
|
|
|
|
if ok {
|
|
h.AuthAdmin = true
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
func (h *Handler) authByEnv(username, password string) (bool, error) {
|
|
hashReader := strings.NewReader(h.Config.AdminLogin)
|
|
|
|
pw, err := htpasswd.NewFromReader(hashReader, htpasswd.DefaultSystems, nil)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if ok := pw.Match(username, password); ok {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// ParseEnvs parses all needed environment variables:
|
|
// DDNS_ADMIN_LOGIN: The basic auth login string in htpasswd style.
|
|
// DDNS_DOMAINS: All domains that will be handled by the dyndns server.
|
|
func (h *Handler) ParseEnvs() (adminAuth bool, err error) {
|
|
log.Info("Read environment variables")
|
|
h.Config = Envs{}
|
|
adminAuth = true
|
|
h.Config.AdminLogin = os.Getenv("DDNS_ADMIN_LOGIN")
|
|
if h.Config.AdminLogin == "" {
|
|
log.Info("No Auth! DDNS_ADMIN_LOGIN should be set")
|
|
adminAuth = false
|
|
h.AuthAdmin = true
|
|
h.DisableAdminAuth = true
|
|
}
|
|
var ok bool
|
|
h.Title, ok = os.LookupEnv("DDNS_TITLE")
|
|
if !ok {
|
|
h.Title = "w3K DynDNS"
|
|
}
|
|
|
|
// ADDED: Check for logo files in the static icons directory upon startup.
|
|
logoExtensions := []string{"png", "webp", "svg"}
|
|
for _, ext := range logoExtensions {
|
|
path := fmt.Sprintf("static/icons/logo.%s", ext)
|
|
if _, err := os.Stat(path); err == nil {
|
|
h.LogoPath = "/" + path // Store the valid path if found
|
|
log.Info("Found logo at: ", h.LogoPath)
|
|
break
|
|
}
|
|
}
|
|
|
|
allowWildcard, ok := os.LookupEnv("DDNS_ALLOW_WILDCARD")
|
|
if ok {
|
|
h.AllowWildcard, err = strconv.ParseBool(allowWildcard)
|
|
if err == nil {
|
|
log.Info("Wildcard allowed")
|
|
}
|
|
}
|
|
logoutUrl, ok := os.LookupEnv("DDNS_LOGOUT_URL")
|
|
if ok {
|
|
if len(logoutUrl) > 0 {
|
|
log.Info("Logout url set: ", logoutUrl)
|
|
h.LogoutUrl = logoutUrl
|
|
}
|
|
}
|
|
|
|
h.PoweredBy, ok = os.LookupEnv("DDNS_POWERED_BY")
|
|
if !ok || h.PoweredBy == "" {
|
|
h.PoweredBy = "w3K LLC"
|
|
} else {
|
|
log.Info("Powered by set: ", h.PoweredBy)
|
|
}
|
|
|
|
h.PoweredByUrl, ok = os.LookupEnv("DDNS_POWERED_BY_URL")
|
|
if !ok || h.PoweredByUrl == "" {
|
|
h.PoweredByUrl = "https://w3K.one/"
|
|
} else {
|
|
log.Info("Powered by URL set: ", h.PoweredByUrl)
|
|
}
|
|
|
|
clearEnv := os.Getenv("DDNS_CLEAR_LOG_INTERVAL")
|
|
clearInterval, err := strconv.ParseUint(clearEnv, 10, 32)
|
|
if err != nil {
|
|
log.Info("No log clear interval found")
|
|
} else {
|
|
log.Info("log clear interval found: ", clearInterval, "days")
|
|
h.ClearInterval = clearInterval
|
|
if clearInterval > 0 {
|
|
h.LastClearedLogs = time.Now()
|
|
}
|
|
}
|
|
|
|
h.Config.Domains = strings.Split(os.Getenv("DDNS_DOMAINS"), ",")
|
|
if len(h.Config.Domains) < 1 {
|
|
return adminAuth, fmt.Errorf("environment variable DDNS_DOMAINS has to be set")
|
|
}
|
|
|
|
// Initialize session store
|
|
if err := h.InitSessionStore(); err != nil {
|
|
return adminAuth, fmt.Errorf("failed to initialize session store: %v", err)
|
|
}
|
|
|
|
return adminAuth, nil
|
|
}
|
|
|
|
// InitDB creates an empty database and creates all tables if there isn't already one, or opens the existing one.
|
|
func (h *Handler) InitDB() (err error) {
|
|
if _, err := os.Stat("database"); os.IsNotExist(err) {
|
|
err = os.MkdirAll("database", os.ModePerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
h.DB, err = gorm.Open(sqlite.Open("database/ddns.db"), &gorm.Config{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Migrate all models including new security models
|
|
err = h.DB.AutoMigrate(
|
|
&model.Host{},
|
|
&model.CName{},
|
|
&model.Log{},
|
|
&model.FailedAuth{}, // NEW: Failed authentication tracking
|
|
&model.BlockedIP{}, // NEW: Blocked IP tracking
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
// Check if a log cleaning is needed
|
|
func (h *Handler) CheckClearInterval() {
|
|
if !h.LastClearedLogs.IsZero() {
|
|
if !DateEqual(time.Now(), h.LastClearedLogs) {
|
|
go h.ClearLogs()
|
|
}
|
|
}
|
|
}
|
|
|
|
// compare two dates
|
|
func DateEqual(date1, date2 time.Time) bool {
|
|
y1, m1, d1 := date1.Date()
|
|
y2, m2, d2 := date2.Date()
|
|
return y1 == y2 && m1 == m2 && d1 == d2
|
|
}
|