feat: Optimize proxy routing and rate limiting configurations
- Adjust rate limiter settings for HTTP and HTTPS servers - Enhance admin container IP handling with specialized update logic - Improve URL parsing and proxy request handling for different domain types - Add more robust IP and header management in reverse proxy - Implement domain-specific proxy routing with improved logging
- commit
- b757931563efa747e83577c69387dc71ac95a6e2
- author
- bamen · · 2025-03-11 14:52 UTC
- parent
- 2358be5
- signature
- unsigned
2 files · +161 -79
M internal/proxy/middleware.go +8 -8
| @@ -58,8 +58,8 @@ func (p *Proxy) setupMiddleware() { | |||
| 58 | 58 | // Create a new Starskey rate limiter store for HTTPS server | |
| 59 | 59 | httpsRateLimiter, err := kv.NewStarskeyRateLimiterStore( | |
| 60 | 60 | "./data/ratelimiter/https", // Path to store rate limit data | |
| 61 | - | 2, // Rate: 2 requests per second (lowered from 10) | |
| 62 | - | 10, // Burst: 10 requests (lowered from 30) | |
| 61 | + | 10, // Rate: 10 requests per second (lowered from 10) | |
| 62 | + | 30, // Burst: 30 requests (lowered from 30) | |
| 63 | 63 | 3*time.Minute, // Expires in 3 minutes | |
| 64 | 64 | ) | |
| 65 | 65 | if err != nil { | |
| @@ -69,8 +69,8 @@ func (p *Proxy) setupMiddleware() { | |||
| 69 | 69 | p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ | |
| 70 | 70 | Store: middleware.NewRateLimiterMemoryStoreWithConfig( | |
| 71 | 71 | middleware.RateLimiterMemoryStoreConfig{ | |
| 72 | - | Rate: rate.Limit(2), // 2 requests per second (lowered from 10) | |
| 73 | - | Burst: 10, // Burst of 10 requests (lowered from 30) | |
| 72 | + | Rate: rate.Limit(10), // 10 requests per second (lowered from 10) | |
| 73 | + | Burst: 30, // Burst of 30 requests (lowered from 30) | |
| 74 | 74 | ExpiresIn: 3 * time.Minute, // Store expiration | |
| 75 | 75 | }, | |
| 76 | 76 | ), | |
| @@ -137,8 +137,8 @@ func (p *Proxy) setupMiddleware() { | |||
| 137 | 137 | // Create a new Starskey rate limiter store for HTTP server | |
| 138 | 138 | httpRateLimiter, err := kv.NewStarskeyRateLimiterStore( | |
| 139 | 139 | "./data/ratelimiter/http", // Path to store rate limit data | |
| 140 | - | 10, // Rate: 10 requests per second (lowered from 10) | |
| 141 | - | 30, // Burst: 30 requests (lowered from 30) | |
| 140 | + | 2, // Rate: 2 requests per second (lowered from 10) | |
| 141 | + | 10, // Burst: 10 requests (lowered from 30) | |
| 142 | 142 | 3*time.Minute, // Expires in 3 minutes | |
| 143 | 143 | ) | |
| 144 | 144 | if err != nil { | |
| @@ -148,8 +148,8 @@ func (p *Proxy) setupMiddleware() { | |||
| 148 | 148 | p.httpServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ | |
| 149 | 149 | Store: middleware.NewRateLimiterMemoryStoreWithConfig( | |
| 150 | 150 | middleware.RateLimiterMemoryStoreConfig{ | |
| 151 | - | Rate: rate.Limit(10), // 10 requests per second (lowered from 10) | |
| 152 | - | Burst: 30, // Burst of 30 requests (lowered from 30) | |
| 151 | + | Rate: rate.Limit(2), // 2 requests per second (lowered from 10) | |
| 152 | + | Burst: 10, // Burst of 10 requests (lowered from 30) | |
| 153 | 153 | ExpiresIn: 3 * time.Minute, // Store expiration | |
| 154 | 154 | }, | |
| 155 | 155 | ), | |
| @@ -58,8 +58,8 @@ func (p *Proxy) setupMiddleware() { | |||||
| 58 | // Create a new Starskey rate limiter store for HTTPS server | 58 | // Create a new Starskey rate limiter store for HTTPS server | ||
| 59 | httpsRateLimiter, err := kv.NewStarskeyRateLimiterStore( | 59 | httpsRateLimiter, err := kv.NewStarskeyRateLimiterStore( | ||
| 60 | "./data/ratelimiter/https", // Path to store rate limit data | 60 | "./data/ratelimiter/https", // Path to store rate limit data | ||
| 61 | - | 2, // Rate: 2 requests per second (lowered from 10) | 61 | + | 10, // Rate: 10 requests per second (lowered from 10) |
| 62 | - | 10, // Burst: 10 requests (lowered from 30) | 62 | + | 30, // Burst: 30 requests (lowered from 30) |
| 63 | 3*time.Minute, // Expires in 3 minutes | 63 | 3*time.Minute, // Expires in 3 minutes | ||
| 64 | ) | 64 | ) | ||
| 65 | if err != nil { | 65 | if err != nil { | ||
| @@ -69,8 +69,8 @@ func (p *Proxy) setupMiddleware() { | |||||
| 69 | p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ | 69 | p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ | ||
| 70 | Store: middleware.NewRateLimiterMemoryStoreWithConfig( | 70 | Store: middleware.NewRateLimiterMemoryStoreWithConfig( | ||
| 71 | middleware.RateLimiterMemoryStoreConfig{ | 71 | middleware.RateLimiterMemoryStoreConfig{ | ||
| 72 | - | Rate: rate.Limit(2), // 2 requests per second (lowered from 10) | 72 | + | Rate: rate.Limit(10), // 10 requests per second (lowered from 10) |
| 73 | - | Burst: 10, // Burst of 10 requests (lowered from 30) | 73 | + | Burst: 30, // Burst of 30 requests (lowered from 30) |
| 74 | ExpiresIn: 3 * time.Minute, // Store expiration | 74 | ExpiresIn: 3 * time.Minute, // Store expiration | ||
| 75 | }, | 75 | }, | ||
| 76 | ), | 76 | ), | ||
| @@ -137,8 +137,8 @@ func (p *Proxy) setupMiddleware() { | |||||
| 137 | // Create a new Starskey rate limiter store for HTTP server | 137 | // Create a new Starskey rate limiter store for HTTP server | ||
| 138 | httpRateLimiter, err := kv.NewStarskeyRateLimiterStore( | 138 | httpRateLimiter, err := kv.NewStarskeyRateLimiterStore( | ||
| 139 | "./data/ratelimiter/http", // Path to store rate limit data | 139 | "./data/ratelimiter/http", // Path to store rate limit data | ||
| 140 | - | 10, // Rate: 10 requests per second (lowered from 10) | 140 | + | 2, // Rate: 2 requests per second (lowered from 10) |
| 141 | - | 30, // Burst: 30 requests (lowered from 30) | 141 | + | 10, // Burst: 10 requests (lowered from 30) |
| 142 | 3*time.Minute, // Expires in 3 minutes | 142 | 3*time.Minute, // Expires in 3 minutes | ||
| 143 | ) | 143 | ) | ||
| 144 | if err != nil { | 144 | if err != nil { | ||
| @@ -148,8 +148,8 @@ func (p *Proxy) setupMiddleware() { | |||||
| 148 | p.httpServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ | 148 | p.httpServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ | ||
| 149 | Store: middleware.NewRateLimiterMemoryStoreWithConfig( | 149 | Store: middleware.NewRateLimiterMemoryStoreWithConfig( | ||
| 150 | middleware.RateLimiterMemoryStoreConfig{ | 150 | middleware.RateLimiterMemoryStoreConfig{ | ||
| 151 | - | Rate: rate.Limit(10), // 10 requests per second (lowered from 10) | 151 | + | Rate: rate.Limit(2), // 2 requests per second (lowered from 10) |
| 152 | - | Burst: 30, // Burst of 30 requests (lowered from 30) | 152 | + | Burst: 10, // Burst of 10 requests (lowered from 30) |
| 153 | ExpiresIn: 3 * time.Minute, // Store expiration | 153 | ExpiresIn: 3 * time.Minute, // Store expiration | ||
| 154 | }, | 154 | }, | ||
| 155 | ), | 155 | ), | ||
M internal/proxy/routes.go +153 -71
| @@ -1104,6 +1104,17 @@ func (p *Proxy) StartContainerEventListener() error { | |||
| 1104 | 1104 | "container_name", containerName, | |
| 1105 | 1105 | "container_ip", containerIP) | |
| 1106 | 1106 | ||
| 1107 | + | // Check if this is the Gordon container (admin domain) | |
| 1108 | + | isAdminContainer := false | |
| 1109 | + | adminDomain := p.app.GetConfig().Http.FullDomain() | |
| 1110 | + | ||
| 1111 | + | p.mu.RLock() | |
| 1112 | + | if route, exists := p.routes[adminDomain]; exists && route.ContainerID == containerID { | |
| 1113 | + | isAdminContainer = true | |
| 1114 | + | logger.Debug("Admin container event detected", "container_id", containerID) | |
| 1115 | + | } | |
| 1116 | + | p.mu.RUnlock() | |
| 1117 | + | ||
| 1107 | 1118 | // First, check if there are any existing routes for this container ID | |
| 1108 | 1119 | var existingRoutes = make(map[string]*ProxyRouteInfo) | |
| 1109 | 1120 | p.mu.RLock() | |
| @@ -1134,7 +1145,67 @@ func (p *Proxy) StartContainerEventListener() error { | |||
| 1134 | 1145 | ||
| 1135 | 1146 | // Check and update each route if IP has changed | |
| 1136 | 1147 | for domain, route := range existingRoutes { | |
| 1137 | - | if route.ContainerIP != containerIP { | |
| 1148 | + | // Handle admin domain specially to avoid path issues | |
| 1149 | + | if domain == adminDomain && isAdminContainer { | |
| 1150 | + | // For admin domain, only update the IP if it has changed | |
| 1151 | + | if route.ContainerIP != containerIP { | |
| 1152 | + | logger.Info("Admin container restarted with new IP - updating route", | |
| 1153 | + | "container_id", containerID, | |
| 1154 | + | "container_name", containerName, | |
| 1155 | + | "domain", domain, | |
| 1156 | + | "old_ip", route.ContainerIP, | |
| 1157 | + | "new_ip", containerIP) | |
| 1158 | + | ||
| 1159 | + | // Update IP in memory only first | |
| 1160 | + | p.mu.Lock() | |
| 1161 | + | if r, ok := p.routes[domain]; ok { | |
| 1162 | + | r.ContainerIP = containerIP | |
| 1163 | + | r.Active = true | |
| 1164 | + | } | |
| 1165 | + | p.mu.Unlock() | |
| 1166 | + | ||
| 1167 | + | // Update DB in background | |
| 1168 | + | go func(domain, containerID, containerIP string) { | |
| 1169 | + | // Get the domain ID | |
| 1170 | + | var domainID string | |
| 1171 | + | rows, err := p.dbQueryWithRetry("SELECT id FROM domain WHERE name = ?", domain) | |
| 1172 | + | if err != nil { | |
| 1173 | + | logger.Error("Failed to query domain for admin IP update", "domain", domain, "error", err) | |
| 1174 | + | return | |
| 1175 | + | } | |
| 1176 | + | ||
| 1177 | + | if !rows.Next() { | |
| 1178 | + | rows.Close() | |
| 1179 | + | logger.Error("Admin domain not found in database", "domain", domain) | |
| 1180 | + | return | |
| 1181 | + | } | |
| 1182 | + | ||
| 1183 | + | if err := rows.Scan(&domainID); err != nil { | |
| 1184 | + | rows.Close() | |
| 1185 | + | logger.Error("Failed to scan admin domain ID", "domain", domain, "error", err) | |
| 1186 | + | return | |
| 1187 | + | } | |
| 1188 | + | rows.Close() | |
| 1189 | + | ||
| 1190 | + | // Update the IP in the database directly | |
| 1191 | + | now := time.Now().Format(time.RFC3339) | |
| 1192 | + | _, err = p.dbExecWithRetry(` | |
| 1193 | + | UPDATE proxy_route SET container_ip = ?, active = 1, updated_at = ? | |
| 1194 | + | WHERE domain_id = ? AND container_id = ?`, | |
| 1195 | + | containerIP, now, domainID, containerID) | |
| 1196 | + | ||
| 1197 | + | if err != nil { | |
| 1198 | + | logger.Error("Failed to update admin container IP in database", "domain", domain, "error", err) | |
| 1199 | + | } else { | |
| 1200 | + | logger.Info("Updated admin container IP in database", | |
| 1201 | + | "domain", domain, | |
| 1202 | + | "container_id", containerID, | |
| 1203 | + | "new_ip", containerIP) | |
| 1204 | + | } | |
| 1205 | + | }(domain, containerID, containerIP) | |
| 1206 | + | } | |
| 1207 | + | } else if route.ContainerIP != containerIP { | |
| 1208 | + | // For non-admin domains, use the standard route update | |
| 1138 | 1209 | logger.Info("Container restarted with new IP - updating route", | |
| 1139 | 1210 | "container_id", containerID, | |
| 1140 | 1211 | "container_name", containerName, | |
| @@ -1702,68 +1773,67 @@ func (p *Proxy) proxyRequest(c echo.Context, route *ProxyRouteInfo) error { | |||
| 1702 | 1773 | containerPort := route.ContainerPort | |
| 1703 | 1774 | targetProtocol := route.Protocol | |
| 1704 | 1775 | ||
| 1705 | - | // Always verify the container IP is current before proxying | |
| 1706 | - | networkName := p.app.GetConfig().ContainerEngine.Network | |
| 1776 | + | // Check if this is the admin domain | |
| 1777 | + | isAdminDomain := host == p.app.GetConfig().Http.FullDomain() | |
| 1707 | 1778 | ||
| 1708 | - | // Get the latest container info to check if IP has changed | |
| 1709 | - | containerInfo, err := docker.GetContainerInfo(containerID) | |
| 1710 | - | if err == nil && containerInfo.NetworkSettings != nil { | |
| 1711 | - | // Check if container has a different IP | |
| 1712 | - | if networkSettings, exists := containerInfo.NetworkSettings.Networks[networkName]; exists && | |
| 1713 | - | networkSettings.IPAddress != "" && networkSettings.IPAddress != containerIP { | |
| 1714 | - | // Found container with different IP, update the route and use new IP for this request | |
| 1715 | - | logger.Warn("Container IP mismatch detected, using live IP instead of cached value", | |
| 1716 | - | "domain", host, | |
| 1717 | - | "cached_ip", containerIP, | |
| 1718 | - | "live_ip", networkSettings.IPAddress) | |
| 1779 | + | // For non-admin domains, verify the container IP is current before proxying | |
| 1780 | + | if !isAdminDomain { | |
| 1781 | + | networkName := p.app.GetConfig().ContainerEngine.Network | |
| 1719 | 1782 | ||
| 1720 | - | // Use the new IP for this request | |
| 1721 | - | containerIP = networkSettings.IPAddress | |
| 1783 | + | // Get the latest container info to check if IP has changed | |
| 1784 | + | containerInfo, err := docker.GetContainerInfo(containerID) | |
| 1785 | + | if err == nil && containerInfo.NetworkSettings != nil { | |
| 1786 | + | // Check if container has a different IP | |
| 1787 | + | if networkSettings, exists := containerInfo.NetworkSettings.Networks[networkName]; exists && | |
| 1788 | + | networkSettings.IPAddress != "" && networkSettings.IPAddress != containerIP { | |
| 1789 | + | // Found container with different IP, update the route and use new IP for this request | |
| 1790 | + | logger.Warn("Container IP mismatch detected, using live IP instead of cached value", | |
| 1791 | + | "domain", host, | |
| 1792 | + | "cached_ip", containerIP, | |
| 1793 | + | "live_ip", networkSettings.IPAddress) | |
| 1722 | 1794 | ||
| 1723 | - | // Update routes in the background | |
| 1724 | - | go func(domain, containerID, containerIP, containerPort string) { | |
| 1725 | - | err := p.ForceUpdateRouteIP(domain, containerIP) | |
| 1726 | - | if err != nil { | |
| 1727 | - | logger.Error("Failed to update IP after mismatch detection", | |
| 1728 | - | "domain", domain, | |
| 1729 | - | "error", err) | |
| 1730 | - | } | |
| 1731 | - | }(host, containerID, containerIP, containerPort) | |
| 1795 | + | // Use the new IP for this request | |
| 1796 | + | containerIP = networkSettings.IPAddress | |
| 1797 | + | ||
| 1798 | + | // Update routes in the background | |
| 1799 | + | go func(domain, containerID, containerIP, containerPort string) { | |
| 1800 | + | err := p.ForceUpdateRouteIP(domain, containerIP) | |
| 1801 | + | if err != nil { | |
| 1802 | + | logger.Error("Failed to update IP after mismatch detection", | |
| 1803 | + | "domain", domain, | |
| 1804 | + | "error", err) | |
| 1805 | + | } | |
| 1806 | + | }(host, containerID, containerIP, containerPort) | |
| 1807 | + | } | |
| 1732 | 1808 | } | |
| 1733 | 1809 | } | |
| 1734 | 1810 | ||
| 1735 | - | // Construct the target URL | |
| 1736 | - | targetURL := fmt.Sprintf("%s://%s:%s", targetProtocol, containerIP, containerPort) | |
| 1737 | - | ||
| 1738 | - | // Add the path from the original request | |
| 1739 | - | if c.Request().URL.Path != "" { | |
| 1740 | - | targetURL += c.Request().URL.Path | |
| 1811 | + | // Create the target URL | |
| 1812 | + | targetURL := &url.URL{ | |
| 1813 | + | Scheme: targetProtocol, | |
| 1741 | 1814 | } | |
| 1742 | 1815 | ||
| 1743 | - | // Add query string if present | |
| 1744 | - | if c.Request().URL.RawQuery != "" { | |
| 1745 | - | targetURL += "?" + c.Request().URL.RawQuery | |
| 1816 | + | // Format host properly for both IPv4 and IPv6 | |
| 1817 | + | if strings.Contains(containerIP, ":") { | |
| 1818 | + | // IPv6 address needs to be wrapped in brackets | |
| 1819 | + | containerIP = "[" + containerIP + "]" | |
| 1746 | 1820 | } | |
| 1821 | + | targetURL.Host = fmt.Sprintf("%s:%s", containerIP, containerPort) | |
| 1747 | 1822 | ||
| 1748 | - | // Parse the target URL | |
| 1749 | - | target, err := url.Parse(targetURL) | |
| 1750 | - | if err != nil { | |
| 1751 | - | logger.Error("Failed to parse target URL", | |
| 1752 | - | "url", targetURL, | |
| 1753 | - | "error", err) | |
| 1754 | - | return echo.NewHTTPError(http.StatusInternalServerError, "Failed to parse target URL") | |
| 1755 | - | } | |
| 1823 | + | // Set the path - keeping it simple to avoid duplication issues | |
| 1824 | + | targetURL.Path = c.Request().URL.Path | |
| 1825 | + | targetURL.RawQuery = c.Request().URL.RawQuery | |
| 1756 | 1826 | ||
| 1757 | - | // Special logging for admin domain | |
| 1758 | - | if host == p.app.GetConfig().Http.FullDomain() { | |
| 1827 | + | // Special logging based on domain type | |
| 1828 | + | if isAdminDomain { | |
| 1759 | 1829 | logger.Debug("Proxying request to admin domain", | |
| 1760 | 1830 | "domain", host, | |
| 1761 | - | "target", targetURL, | |
| 1831 | + | "target", targetURL.String(), | |
| 1762 | 1832 | "path", c.Request().URL.Path) | |
| 1763 | 1833 | } else { | |
| 1764 | - | logger.Debug("Proxying request host", | |
| 1834 | + | logger.Debug("Proxying request to container", | |
| 1765 | 1835 | "host", host, | |
| 1766 | - | "target", targetURL, | |
| 1836 | + | "target", targetURL.String(), | |
| 1767 | 1837 | "path", c.Request().URL.Path, | |
| 1768 | 1838 | "clientIP", c.RealIP(), | |
| 1769 | 1839 | "remoteAddr", c.Request().RemoteAddr, | |
| @@ -1771,7 +1841,7 @@ func (p *Proxy) proxyRequest(c echo.Context, route *ProxyRouteInfo) error { | |||
| 1771 | 1841 | } | |
| 1772 | 1842 | ||
| 1773 | 1843 | // Create a reverse proxy | |
| 1774 | - | proxy := httputil.NewSingleHostReverseProxy(target) | |
| 1844 | + | proxy := httputil.NewSingleHostReverseProxy(targetURL) | |
| 1775 | 1845 | ||
| 1776 | 1846 | // Use our custom transport with longer timeouts | |
| 1777 | 1847 | if p.reverseProxyClient != nil { | |
| @@ -1784,10 +1854,16 @@ func (p *Proxy) proxyRequest(c echo.Context, route *ProxyRouteInfo) error { | |||
| 1784 | 1854 | // Call the original director first | |
| 1785 | 1855 | originalDirector(req) | |
| 1786 | 1856 | ||
| 1787 | - | // Update the Host header to the target host | |
| 1788 | - | req.Host = target.Host | |
| 1857 | + | // Handle the Host header differently based on domain type | |
| 1858 | + | if isAdminDomain { | |
| 1859 | + | // For admin domain, use the target host for internal routing | |
| 1860 | + | req.Host = targetURL.Host | |
| 1861 | + | } else { | |
| 1862 | + | // For other domains, keep the original host | |
| 1863 | + | req.Host = host | |
| 1864 | + | } | |
| 1789 | 1865 | ||
| 1790 | - | // Set X-Forwarded-For | |
| 1866 | + | // Set X-Forwarded headers to preserve client information | |
| 1791 | 1867 | if clientIP := c.RealIP(); clientIP != "" { | |
| 1792 | 1868 | req.Header.Set("X-Forwarded-For", clientIP) | |
| 1793 | 1869 | req.Header.Set("X-Real-IP", clientIP) | |
| @@ -1796,6 +1872,9 @@ func (p *Proxy) proxyRequest(c echo.Context, route *ProxyRouteInfo) error { | |||
| 1796 | 1872 | // Set X-Forwarded-Proto to the original protocol | |
| 1797 | 1873 | req.Header.Set("X-Forwarded-Proto", c.Scheme()) | |
| 1798 | 1874 | ||
| 1875 | + | // Set X-Forwarded-Host to the original host | |
| 1876 | + | req.Header.Set("X-Forwarded-Host", host) | |
| 1877 | + | ||
| 1799 | 1878 | // Log request headers for debugging | |
| 1800 | 1879 | logger.Debug("Request headers for proxy", | |
| 1801 | 1880 | "domain", host, | |
| @@ -1807,30 +1886,33 @@ func (p *Proxy) proxyRequest(c echo.Context, route *ProxyRouteInfo) error { | |||
| 1807 | 1886 | proxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, err error) { | |
| 1808 | 1887 | logger.Error("Proxy error", | |
| 1809 | 1888 | "host", host, | |
| 1810 | - | "target", targetURL, | |
| 1889 | + | "target", targetURL.String(), | |
| 1811 | 1890 | "error", err) | |
| 1812 | 1891 | ||
| 1813 | - | // Check if this is a connection refused error | |
| 1814 | - | if strings.Contains(err.Error(), "connection refused") || | |
| 1815 | - | strings.Contains(err.Error(), "no route to host") || | |
| 1816 | - | strings.Contains(err.Error(), "i/o timeout") { | |
| 1817 | - | // Container might be down or unreachable - mark the route as inactive | |
| 1818 | - | logger.Warn("Container appears to be down, marking route as inactive", | |
| 1819 | - | "domain", host, | |
| 1820 | - | "container_id", containerID) | |
| 1821 | - | ||
| 1822 | - | // Find domains for this container and mark them inactive | |
| 1823 | - | var domains []string | |
| 1824 | - | p.mu.RLock() | |
| 1825 | - | for d, r := range p.routes { | |
| 1826 | - | if r.ContainerID == containerID { | |
| 1827 | - | domains = append(domains, d) | |
| 1892 | + | // Skip container down detection for admin domain | |
| 1893 | + | if !isAdminDomain { | |
| 1894 | + | // Check if this is a connection refused error | |
| 1895 | + | if strings.Contains(err.Error(), "connection refused") || | |
| 1896 | + | strings.Contains(err.Error(), "no route to host") || | |
| 1897 | + | strings.Contains(err.Error(), "i/o timeout") { | |
| 1898 | + | // Container might be down or unreachable - mark the route as inactive | |
| 1899 | + | logger.Warn("Container appears to be down, marking route as inactive", | |
| 1900 | + | "domain", host, | |
| 1901 | + | "container_id", containerID) | |
| 1902 | + | ||
| 1903 | + | // Find domains for this container and mark them inactive | |
| 1904 | + | var domains []string | |
| 1905 | + | p.mu.RLock() | |
| 1906 | + | for d, r := range p.routes { | |
| 1907 | + | if r.ContainerID == containerID { | |
| 1908 | + | domains = append(domains, d) | |
| 1909 | + | } | |
| 1828 | 1910 | } | |
| 1829 | - | } | |
| 1830 | - | p.mu.RUnlock() | |
| 1911 | + | p.mu.RUnlock() | |
| 1831 | 1912 | ||
| 1832 | - | if len(domains) > 0 { | |
| 1833 | - | go p.markRoutesInactive(domains) | |
| 1913 | + | if len(domains) > 0 { | |
| 1914 | + | go p.markRoutesInactive(domains) | |
| 1915 | + | } | |
| 1834 | 1916 | } | |
| 1835 | 1917 | } | |
| 1836 | 1918 | ||
| @@ -1104,6 +1104,17 @@ func (p *Proxy) StartContainerEventListener() error { | |||||
| 1104 | "container_name", containerName, | 1104 | "container_name", containerName, | ||
| 1105 | "container_ip", containerIP) | 1105 | "container_ip", containerIP) | ||
| 1106 | 1106 | ||||
| 1107 | + | // Check if this is the Gordon container (admin domain) | |||
| 1108 | + | isAdminContainer := false | |||
| 1109 | + | adminDomain := p.app.GetConfig().Http.FullDomain() | |||
| 1110 | + | ||||
| 1111 | + | p.mu.RLock() | |||
| 1112 | + | if route, exists := p.routes[adminDomain]; exists && route.ContainerID == containerID { | |||
| 1113 | + | isAdminContainer = true | |||
| 1114 | + | logger.Debug("Admin container event detected", "container_id", containerID) | |||
| 1115 | + | } | |||
| 1116 | + | p.mu.RUnlock() | |||
| 1117 | + | ||||
| 1107 | // First, check if there are any existing routes for this container ID | 1118 | // First, check if there are any existing routes for this container ID | ||
| 1108 | var existingRoutes = make(map[string]*ProxyRouteInfo) | 1119 | var existingRoutes = make(map[string]*ProxyRouteInfo) | ||
| 1109 | p.mu.RLock() | 1120 | p.mu.RLock() | ||
| @@ -1134,7 +1145,67 @@ func (p *Proxy) StartContainerEventListener() error { | |||||
| 1134 | 1145 | ||||
| 1135 | // Check and update each route if IP has changed | 1146 | // Check and update each route if IP has changed | ||
| 1136 | for domain, route := range existingRoutes { | 1147 | for domain, route := range existingRoutes { | ||
| 1137 | - | if route.ContainerIP != containerIP { | 1148 | + | // Handle admin domain specially to avoid path issues |
| 1149 | + | if domain == adminDomain && isAdminContainer { | |||
| 1150 | + | // For admin domain, only update the IP if it has changed | |||
| 1151 | + | if route.ContainerIP != containerIP { | |||
| 1152 | + | logger.Info("Admin container restarted with new IP - updating route", | |||
| 1153 | + | "container_id", containerID, | |||
| 1154 | + | "container_name", containerName, | |||
| 1155 | + | "domain", domain, | |||
| 1156 | + | "old_ip", route.ContainerIP, | |||
| 1157 | + | "new_ip", containerIP) | |||
| 1158 | + | ||||
| 1159 | + | // Update IP in memory only first | |||
| 1160 | + | p.mu.Lock() | |||
| 1161 | + | if r, ok := p.routes[domain]; ok { | |||
| 1162 | + | r.ContainerIP = containerIP | |||
| 1163 | + | r.Active = true | |||
| 1164 | + | } | |||
| 1165 | + | p.mu.Unlock() | |||
| 1166 | + | ||||
| 1167 | + | // Update DB in background | |||
| 1168 | + | go func(domain, containerID, containerIP string) { | |||
| 1169 | + | // Get the domain ID | |||
| 1170 | + | var domainID string | |||
| 1171 | + | rows, err := p.dbQueryWithRetry("SELECT id FROM domain WHERE name = ?", domain) | |||
| 1172 | + | if err != nil { | |||
| 1173 | + | logger.Error("Failed to query domain for admin IP update", "domain", domain, "error", err) | |||
| 1174 | + | return | |||
| 1175 | + | } | |||
| 1176 | + | ||||
| 1177 | + | if !rows.Next() { | |||
| 1178 | + | rows.Close() | |||
| 1179 | + | logger.Error("Admin domain not found in database", "domain", domain) | |||
| 1180 | + | return | |||
| 1181 | + | } | |||
| 1182 | + | ||||
| 1183 | + | if err := rows.Scan(&domainID); err != nil { | |||
| 1184 | + | rows.Close() | |||
| 1185 | + | logger.Error("Failed to scan admin domain ID", "domain", domain, "error", err) | |||
| 1186 | + | return | |||
| 1187 | + | } | |||
| 1188 | + | rows.Close() | |||
| 1189 | + | ||||
| 1190 | + | // Update the IP in the database directly | |||
| 1191 | + | now := time.Now().Format(time.RFC3339) | |||
| 1192 | + | _, err = p.dbExecWithRetry(` | |||
| 1193 | + | UPDATE proxy_route SET container_ip = ?, active = 1, updated_at = ? | |||
| 1194 | + | WHERE domain_id = ? AND container_id = ?`, | |||
| 1195 | + | containerIP, now, domainID, containerID) | |||
| 1196 | + | ||||
| 1197 | + | if err != nil { | |||
| 1198 | + | logger.Error("Failed to update admin container IP in database", "domain", domain, "error", err) | |||
| 1199 | + | } else { | |||
| 1200 | + | logger.Info("Updated admin container IP in database", | |||
| 1201 | + | "domain", domain, | |||
| 1202 | + | "container_id", containerID, | |||
| 1203 | + | "new_ip", containerIP) | |||
| 1204 | + | } | |||
| 1205 | + | }(domain, containerID, containerIP) | |||
| 1206 | + | } | |||
| 1207 | + | } else if route.ContainerIP != containerIP { | |||
| 1208 | + | // For non-admin domains, use the standard route update | |||
| 1138 | logger.Info("Container restarted with new IP - updating route", | 1209 | logger.Info("Container restarted with new IP - updating route", | ||
| 1139 | "container_id", containerID, | 1210 | "container_id", containerID, | ||
| 1140 | "container_name", containerName, | 1211 | "container_name", containerName, | ||
| @@ -1702,68 +1773,67 @@ func (p *Proxy) proxyRequest(c echo.Context, route *ProxyRouteInfo) error { | |||||
| 1702 | containerPort := route.ContainerPort | 1773 | containerPort := route.ContainerPort | ||
| 1703 | targetProtocol := route.Protocol | 1774 | targetProtocol := route.Protocol | ||
| 1704 | 1775 | ||||
| 1705 | - | // Always verify the container IP is current before proxying | 1776 | + | // Check if this is the admin domain |
| 1706 | - | networkName := p.app.GetConfig().ContainerEngine.Network | 1777 | + | isAdminDomain := host == p.app.GetConfig().Http.FullDomain() |
| 1707 | 1778 | ||||
| 1708 | - | // Get the latest container info to check if IP has changed | 1779 | + | // For non-admin domains, verify the container IP is current before proxying |
| 1709 | - | containerInfo, err := docker.GetContainerInfo(containerID) | 1780 | + | if !isAdminDomain { |
| 1710 | - | if err == nil && containerInfo.NetworkSettings != nil { | 1781 | + | networkName := p.app.GetConfig().ContainerEngine.Network |
| 1711 | - | // Check if container has a different IP | |||
| 1712 | - | if networkSettings, exists := containerInfo.NetworkSettings.Networks[networkName]; exists && | |||
| 1713 | - | networkSettings.IPAddress != "" && networkSettings.IPAddress != containerIP { | |||
| 1714 | - | // Found container with different IP, update the route and use new IP for this request | |||
| 1715 | - | logger.Warn("Container IP mismatch detected, using live IP instead of cached value", | |||
| 1716 | - | "domain", host, | |||
| 1717 | - | "cached_ip", containerIP, | |||
| 1718 | - | "live_ip", networkSettings.IPAddress) | |||
| 1719 | 1782 | ||||
| 1720 | - | // Use the new IP for this request | 1783 | + | // Get the latest container info to check if IP has changed |
| 1721 | - | containerIP = networkSettings.IPAddress | 1784 | + | containerInfo, err := docker.GetContainerInfo(containerID) |
| 1785 | + | if err == nil && containerInfo.NetworkSettings != nil { | |||
| 1786 | + | // Check if container has a different IP | |||
| 1787 | + | if networkSettings, exists := containerInfo.NetworkSettings.Networks[networkName]; exists && | |||
| 1788 | + | networkSettings.IPAddress != "" && networkSettings.IPAddress != containerIP { | |||
| 1789 | + | // Found container with different IP, update the route and use new IP for this request | |||
| 1790 | + | logger.Warn("Container IP mismatch detected, using live IP instead of cached value", | |||
| 1791 | + | "domain", host, | |||
| 1792 | + | "cached_ip", containerIP, | |||
| 1793 | + | "live_ip", networkSettings.IPAddress) | |||
| 1722 | 1794 | ||||
| 1723 | - | // Update routes in the background | 1795 | + | // Use the new IP for this request |
| 1724 | - | go func(domain, containerID, containerIP, containerPort string) { | 1796 | + | containerIP = networkSettings.IPAddress |
| 1725 | - | err := p.ForceUpdateRouteIP(domain, containerIP) | 1797 | + | |
| 1726 | - | if err != nil { | 1798 | + | // Update routes in the background |
| 1727 | - | logger.Error("Failed to update IP after mismatch detection", | 1799 | + | go func(domain, containerID, containerIP, containerPort string) { |
| 1728 | - | "domain", domain, | 1800 | + | err := p.ForceUpdateRouteIP(domain, containerIP) |
| 1729 | - | "error", err) | 1801 | + | if err != nil { |
| 1730 | - | } | 1802 | + | logger.Error("Failed to update IP after mismatch detection", |
| 1731 | - | }(host, containerID, containerIP, containerPort) | 1803 | + | "domain", domain, |
| 1804 | + | "error", err) | |||
| 1805 | + | } | |||
| 1806 | + | }(host, containerID, containerIP, containerPort) | |||
| 1807 | + | } | |||
| 1732 | } | 1808 | } | ||
| 1733 | } | 1809 | } | ||
| 1734 | 1810 | ||||
| 1735 | - | // Construct the target URL | 1811 | + | // Create the target URL |
| 1736 | - | targetURL := fmt.Sprintf("%s://%s:%s", targetProtocol, containerIP, containerPort) | 1812 | + | targetURL := &url.URL{ |
| 1737 | - | 1813 | + | Scheme: targetProtocol, | |
| 1738 | - | // Add the path from the original request | |||
| 1739 | - | if c.Request().URL.Path != "" { | |||
| 1740 | - | targetURL += c.Request().URL.Path | |||
| 1741 | } | 1814 | } | ||
| 1742 | 1815 | ||||
| 1743 | - | // Add query string if present | 1816 | + | // Format host properly for both IPv4 and IPv6 |
| 1744 | - | if c.Request().URL.RawQuery != "" { | 1817 | + | if strings.Contains(containerIP, ":") { |
| 1745 | - | targetURL += "?" + c.Request().URL.RawQuery | 1818 | + | // IPv6 address needs to be wrapped in brackets |
| 1819 | + | containerIP = "[" + containerIP + "]" | |||
| 1746 | } | 1820 | } | ||
| 1821 | + | targetURL.Host = fmt.Sprintf("%s:%s", containerIP, containerPort) | |||
| 1747 | 1822 | ||||
| 1748 | - | // Parse the target URL | 1823 | + | // Set the path - keeping it simple to avoid duplication issues |
| 1749 | - | target, err := url.Parse(targetURL) | 1824 | + | targetURL.Path = c.Request().URL.Path |
| 1750 | - | if err != nil { | 1825 | + | targetURL.RawQuery = c.Request().URL.RawQuery |
| 1751 | - | logger.Error("Failed to parse target URL", | |||
| 1752 | - | "url", targetURL, | |||
| 1753 | - | "error", err) | |||
| 1754 | - | return echo.NewHTTPError(http.StatusInternalServerError, "Failed to parse target URL") | |||
| 1755 | - | } | |||
| 1756 | 1826 | ||||
| 1757 | - | // Special logging for admin domain | 1827 | + | // Special logging based on domain type |
| 1758 | - | if host == p.app.GetConfig().Http.FullDomain() { | 1828 | + | if isAdminDomain { |
| 1759 | logger.Debug("Proxying request to admin domain", | 1829 | logger.Debug("Proxying request to admin domain", | ||
| 1760 | "domain", host, | 1830 | "domain", host, | ||
| 1761 | - | "target", targetURL, | 1831 | + | "target", targetURL.String(), |
| 1762 | "path", c.Request().URL.Path) | 1832 | "path", c.Request().URL.Path) | ||
| 1763 | } else { | 1833 | } else { | ||
| 1764 | - | logger.Debug("Proxying request host", | 1834 | + | logger.Debug("Proxying request to container", |
| 1765 | "host", host, | 1835 | "host", host, | ||
| 1766 | - | "target", targetURL, | 1836 | + | "target", targetURL.String(), |
| 1767 | "path", c.Request().URL.Path, | 1837 | "path", c.Request().URL.Path, | ||
| 1768 | "clientIP", c.RealIP(), | 1838 | "clientIP", c.RealIP(), | ||
| 1769 | "remoteAddr", c.Request().RemoteAddr, | 1839 | "remoteAddr", c.Request().RemoteAddr, | ||
| @@ -1771,7 +1841,7 @@ func (p *Proxy) proxyRequest(c echo.Context, route *ProxyRouteInfo) error { | |||||
| 1771 | } | 1841 | } | ||
| 1772 | 1842 | ||||
| 1773 | // Create a reverse proxy | 1843 | // Create a reverse proxy | ||
| 1774 | - | proxy := httputil.NewSingleHostReverseProxy(target) | 1844 | + | proxy := httputil.NewSingleHostReverseProxy(targetURL) |
| 1775 | 1845 | ||||
| 1776 | // Use our custom transport with longer timeouts | 1846 | // Use our custom transport with longer timeouts | ||
| 1777 | if p.reverseProxyClient != nil { | 1847 | if p.reverseProxyClient != nil { | ||
| @@ -1784,10 +1854,16 @@ func (p *Proxy) proxyRequest(c echo.Context, route *ProxyRouteInfo) error { | |||||
| 1784 | // Call the original director first | 1854 | // Call the original director first | ||
| 1785 | originalDirector(req) | 1855 | originalDirector(req) | ||
| 1786 | 1856 | ||||
| 1787 | - | // Update the Host header to the target host | 1857 | + | // Handle the Host header differently based on domain type |
| 1788 | - | req.Host = target.Host | 1858 | + | if isAdminDomain { |
| 1859 | + | // For admin domain, use the target host for internal routing | |||
| 1860 | + | req.Host = targetURL.Host | |||
| 1861 | + | } else { | |||
| 1862 | + | // For other domains, keep the original host | |||
| 1863 | + | req.Host = host | |||
| 1864 | + | } | |||
| 1789 | 1865 | ||||
| 1790 | - | // Set X-Forwarded-For | 1866 | + | // Set X-Forwarded headers to preserve client information |
| 1791 | if clientIP := c.RealIP(); clientIP != "" { | 1867 | if clientIP := c.RealIP(); clientIP != "" { | ||
| 1792 | req.Header.Set("X-Forwarded-For", clientIP) | 1868 | req.Header.Set("X-Forwarded-For", clientIP) | ||
| 1793 | req.Header.Set("X-Real-IP", clientIP) | 1869 | req.Header.Set("X-Real-IP", clientIP) | ||
| @@ -1796,6 +1872,9 @@ func (p *Proxy) proxyRequest(c echo.Context, route *ProxyRouteInfo) error { | |||||
| 1796 | // Set X-Forwarded-Proto to the original protocol | 1872 | // Set X-Forwarded-Proto to the original protocol | ||
| 1797 | req.Header.Set("X-Forwarded-Proto", c.Scheme()) | 1873 | req.Header.Set("X-Forwarded-Proto", c.Scheme()) | ||
| 1798 | 1874 | ||||
| 1875 | + | // Set X-Forwarded-Host to the original host | |||
| 1876 | + | req.Header.Set("X-Forwarded-Host", host) | |||
| 1877 | + | ||||
| 1799 | // Log request headers for debugging | 1878 | // Log request headers for debugging | ||
| 1800 | logger.Debug("Request headers for proxy", | 1879 | logger.Debug("Request headers for proxy", | ||
| 1801 | "domain", host, | 1880 | "domain", host, | ||
| @@ -1807,30 +1886,33 @@ func (p *Proxy) proxyRequest(c echo.Context, route *ProxyRouteInfo) error { | |||||
| 1807 | proxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, err error) { | 1886 | proxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, err error) { | ||
| 1808 | logger.Error("Proxy error", | 1887 | logger.Error("Proxy error", | ||
| 1809 | "host", host, | 1888 | "host", host, | ||
| 1810 | - | "target", targetURL, | 1889 | + | "target", targetURL.String(), |
| 1811 | "error", err) | 1890 | "error", err) | ||
| 1812 | 1891 | ||||
| 1813 | - | // Check if this is a connection refused error | 1892 | + | // Skip container down detection for admin domain |
| 1814 | - | if strings.Contains(err.Error(), "connection refused") || | 1893 | + | if !isAdminDomain { |
| 1815 | - | strings.Contains(err.Error(), "no route to host") || | 1894 | + | // Check if this is a connection refused error |
| 1816 | - | strings.Contains(err.Error(), "i/o timeout") { | 1895 | + | if strings.Contains(err.Error(), "connection refused") || |
| 1817 | - | // Container might be down or unreachable - mark the route as inactive | 1896 | + | strings.Contains(err.Error(), "no route to host") || |
| 1818 | - | logger.Warn("Container appears to be down, marking route as inactive", | 1897 | + | strings.Contains(err.Error(), "i/o timeout") { |
| 1819 | - | "domain", host, | 1898 | + | // Container might be down or unreachable - mark the route as inactive |
| 1820 | - | "container_id", containerID) | 1899 | + | logger.Warn("Container appears to be down, marking route as inactive", |
| 1821 | - | 1900 | + | "domain", host, | |
| 1822 | - | // Find domains for this container and mark them inactive | 1901 | + | "container_id", containerID) |
| 1823 | - | var domains []string | 1902 | + | |
| 1824 | - | p.mu.RLock() | 1903 | + | // Find domains for this container and mark them inactive |
| 1825 | - | for d, r := range p.routes { | 1904 | + | var domains []string |
| 1826 | - | if r.ContainerID == containerID { | 1905 | + | p.mu.RLock() |
| 1827 | - | domains = append(domains, d) | 1906 | + | for d, r := range p.routes { |
| 1907 | + | if r.ContainerID == containerID { | |||
| 1908 | + | domains = append(domains, d) | |||
| 1909 | + | } | |||
| 1828 | } | 1910 | } | ||
| 1829 | - | } | 1911 | + | p.mu.RUnlock() |
| 1830 | - | p.mu.RUnlock() | |||
| 1831 | 1912 | ||||
| 1832 | - | if len(domains) > 0 { | 1913 | + | if len(domains) > 0 { |
| 1833 | - | go p.markRoutesInactive(domains) | 1914 | + | go p.markRoutesInactive(domains) |
| 1915 | + | } | |||
| 1834 | } | 1916 | } | ||
| 1835 | } | 1917 | } | ||
| 1836 | 1918 | ||||