Add DisableAutoFallback option for proxy endpoints #896

- Added new behavior in upstream random selector (ignore offline check for upstream if there is only one upstream for this proxy rule)
- Added config file only DisableAutoFallback option
This commit is contained in:
Toby Chui
2025-11-18 22:51:55 +08:00
parent d049108f74
commit db407d497f
8 changed files with 77 additions and 33 deletions

View File

@@ -47,23 +47,24 @@ func GetDefaultProxyEndpoint() ProxyEndpoint {
randomPrefix := uuid.New().String()
return ProxyEndpoint{
ProxyType: ProxyTypeHost,
RootOrMatchingDomain: randomPrefix + ".internal",
MatchingDomainAlias: []string{},
ActiveOrigins: []*loadbalance.Upstream{},
InactiveOrigins: []*loadbalance.Upstream{},
UseStickySession: false,
UseActiveLoadBalance: false,
Disabled: false,
BypassGlobalTLS: false,
VirtualDirectories: []*VirtualDirectoryEndpoint{},
HeaderRewriteRules: GetDefaultHeaderRewriteRules(),
EnableWebsocketCustomHeaders: false,
AuthenticationProvider: GetDefaultAuthenticationProvider(),
RequireRateLimit: false,
RateLimit: 0,
DisableUptimeMonitor: false,
AccessFilterUUID: "default",
DefaultSiteOption: DefaultSite_InternalStaticWebServer,
DefaultSiteValue: "",
}
RootOrMatchingDomain: randomPrefix + ".internal",
MatchingDomainAlias: []string{},
ActiveOrigins: []*loadbalance.Upstream{},
InactiveOrigins: []*loadbalance.Upstream{},
UseStickySession: false,
UseActiveLoadBalance: false,
Disabled: false,
BypassGlobalTLS: false,
VirtualDirectories: []*VirtualDirectoryEndpoint{},
HeaderRewriteRules: GetDefaultHeaderRewriteRules(),
EnableWebsocketCustomHeaders: false,
AuthenticationProvider: GetDefaultAuthenticationProvider(),
RequireRateLimit: false,
RateLimit: 0,
DisableUptimeMonitor: false,
DisableAutoFallback: false,
AccessFilterUUID: "default",
DefaultSiteOption: DefaultSite_InternalStaticWebServer,
DefaultSiteValue: "",
}
}

View File

@@ -152,7 +152,7 @@ func (router *Router) StartProxyService() error {
}
}
selectedUpstream, err := router.loadBalancer.GetRequestUpstreamTarget(w, r, sep.ActiveOrigins, sep.UseStickySession)
selectedUpstream, err := router.loadBalancer.GetRequestUpstreamTarget(w, r, sep.ActiveOrigins, sep.UseStickySession, sep.DisableAutoFallback)
if err != nil {
http.ServeFile(w, r, "./web/hosterror.html")
router.Option.Logger.PrintAndLog("dprouter", "failed to get upstream for hostname", err)

View File

@@ -61,7 +61,20 @@ func (m *RouteManager) NotifyHostUnreachableWithTimeout(upstreamIp string, timeo
*/
// FilterOfflineOrigins return only online origins from a list of origins
func (m *RouteManager) FilterOfflineOrigins(origins []*Upstream) []*Upstream {
// If originalUpstreamCount is 1 (only one upstream), or disableAutoFallback is true, return all origins unchanged
func (m *RouteManager) FilterOfflineOrigins(origins []*Upstream, originalUpstreamCount int, disableAutoFallback bool) []*Upstream {
// If there's only one upstream, don't filter it out even if it's offline
// This prevents the 521 error when upgrading/restarting the only upstream server
if originalUpstreamCount == 1 {
return origins
}
// If auto-fallback is disabled, don't filter offline origins
// This allows uptime monitoring to continue without automatically disabling upstreams
if disableAutoFallback {
return origins
}
var onlineOrigins []*Upstream
for _, origin := range origins {
if m.IsTargetOnline(origin.OriginIpOrDomain) {

View File

@@ -19,7 +19,7 @@ const (
// GetRequestUpstreamTarget return the upstream target where this
// request should be routed
func (m *RouteManager) GetRequestUpstreamTarget(w http.ResponseWriter, r *http.Request, origins []*Upstream, useStickySession bool) (*Upstream, error) {
func (m *RouteManager) GetRequestUpstreamTarget(w http.ResponseWriter, r *http.Request, origins []*Upstream, useStickySession bool, disableAutoFallback bool) (*Upstream, error) {
if len(origins) == 0 {
return nil, errors.New("no upstream is defined for this host")
}
@@ -30,8 +30,9 @@ func (m *RouteManager) GetRequestUpstreamTarget(w http.ResponseWriter, r *http.R
targetOriginId, err := m.getSessionHandler(r, origins)
if err != nil {
// No valid session found or origin is offline
// Filter the offline origins
origins = m.FilterOfflineOrigins(origins)
// Filter the offline origins (but only if there's more than 1 upstream and auto-fallback is not disabled)
originalUpstreamCount := len(origins)
origins = m.FilterOfflineOrigins(origins, originalUpstreamCount, disableAutoFallback)
if len(origins) == 0 {
return nil, errors.New("no online upstream is available for origin: " + r.Host)
}
@@ -55,8 +56,9 @@ func (m *RouteManager) GetRequestUpstreamTarget(w http.ResponseWriter, r *http.R
}
//No sticky session, get a random origin
//Filter the offline origins
origins = m.FilterOfflineOrigins(origins)
//Filter the offline origins (but only if there's more than 1 upstream and auto-fallback is not disabled)
originalUpstreamCount := len(origins)
origins = m.FilterOfflineOrigins(origins, originalUpstreamCount, disableAutoFallback)
if len(origins) == 0 {
return nil, errors.New("no online upstream is available for origin: " + r.Host)
}
@@ -73,8 +75,9 @@ func (m *RouteManager) GetRequestUpstreamTarget(w http.ResponseWriter, r *http.R
}
// GetUsableUpstreamCounts return the number of usable upstreams
func (m *RouteManager) GetUsableUpstreamCounts(origins []*Upstream) int {
origins = m.FilterOfflineOrigins(origins)
func (m *RouteManager) GetUsableUpstreamCounts(origins []*Upstream, disableAutoFallback bool) int {
originalUpstreamCount := len(origins)
origins = m.FilterOfflineOrigins(origins, originalUpstreamCount, disableAutoFallback)
return len(origins)
}

View File

@@ -155,7 +155,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
reqHostname := r.Host
/* Load balancing */
selectedUpstream, err := h.Parent.loadBalancer.GetRequestUpstreamTarget(w, r, target.ActiveOrigins, target.UseStickySession)
selectedUpstream, err := h.Parent.loadBalancer.GetRequestUpstreamTarget(w, r, target.ActiveOrigins, target.UseStickySession, target.DisableAutoFallback)
if err != nil {
http.ServeFile(w, r, "./web/rperror.html")
h.Parent.Option.Logger.PrintAndLog("proxy", "Failed to assign an upstream for this request", err)

View File

@@ -210,6 +210,7 @@ type ProxyEndpoint struct {
//Uptime Monitor
DisableUptimeMonitor bool //Disable uptime monitor for this endpoint
DisableAutoFallback bool //Disable automatic fallback when uptime monitor detects an upstream is down (continue monitoring but don't auto-disable upstream)
DisableLogging bool //Disable logging of reverse proxy requests
DisableStatisticCollection bool //Disable statistic collection for this endpoint

View File

@@ -428,6 +428,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
Tags: tags,
DisableUptimeMonitor: !enableUtm,
DisableAutoFallback: false, // Default to false for new endpoints
DisableLogging: disableLog,
BlockCommonExploits: blockCommonExploits,
BlockAICrawlers: blockAICrawlers,
@@ -627,20 +628,24 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
}
}
if authProviderType == 1 {
switch authProviderType {
case 1:
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodBasic
} else if authProviderType == 2 {
case 2:
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodForward
} else if authProviderType == 3 {
case 3:
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodOauth2
} else {
default:
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodNone
}
disableAutoFallback, _ := utils.PostBool(r, "dAutoFallback")
newProxyEndpoint.RequireRateLimit = requireRateLimit
newProxyEndpoint.RateLimit = proxyRateLimit
newProxyEndpoint.UseStickySession = useStickySession
newProxyEndpoint.DisableUptimeMonitor = disbleUtm
newProxyEndpoint.DisableAutoFallback = disableAutoFallback
newProxyEndpoint.DisableChunkedTransferEncoding = disableChunkedEncoding
newProxyEndpoint.DisableLogging = disableLogging
newProxyEndpoint.DisableStatisticCollection = disableStatisticCollection

View File

@@ -316,6 +316,12 @@
<small>Enable active uptime monitor and auto disable upstreams that are offline</small></label>
</div>
<br>
<!-- <div class="ui checkbox" style="margin-top: 0.4em; margin-left: 2em;">
<input type="checkbox" class="DisableAutoFallback">
<label>Disable Auto Fallback<br>
<small>Continue monitoring but don't automatically disable upstreams when offline</small></label>
</div>
<br> -->
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="UseStickySession">
<label>Use Sticky Session<br>
@@ -924,6 +930,7 @@
let epttype = "host";
let useStickySession = $(editor).find(".UseStickySession")[0].checked;
let DisableUptimeMonitor = !$(editor).find(".EnableUptimeMonitor")[0].checked;
let disableAutoFallback = false; //$(editor).find(".DisableAutoFallback")[0].checked;
let authProviderType = $(editor).find(".authProviderPicker input[type='radio']:checked").val();
let requireRateLimit = $(editor).find(".RequireRateLimit")[0].checked;
let rateLimit = $(editor).find(".RateLimit").val();
@@ -946,6 +953,7 @@
"rootname": uuid,
"ss":useStickySession,
"dutm": DisableUptimeMonitor,
"dAutoFallback": disableAutoFallback,
"bpgtls": bypassGlobalTLS,
"authprovider" :authProviderType,
"rate" :requireRateLimit,
@@ -1408,6 +1416,19 @@
editor.find(".EnableUptimeMonitor").off("change");
editor.find(".EnableUptimeMonitor").prop("checked", !subd.DisableUptimeMonitor);
editor.find(".EnableUptimeMonitor").on("change", function() {
// Enable/disable the auto fallback option based on uptime monitor state
var isUptimeMonitorEnabled = $(this).is(":checked");
editor.find(".DisableAutoFallback").prop("disabled", !isUptimeMonitorEnabled);
if (!isUptimeMonitorEnabled) {
editor.find(".DisableAutoFallback").prop("checked", false);
}
saveProxyInlineEdit(uuid);
});
editor.find(".DisableAutoFallback").off("change");
editor.find(".DisableAutoFallback").prop("checked", subd.DisableAutoFallback);
editor.find(".DisableAutoFallback").prop("disabled", subd.DisableUptimeMonitor);
editor.find(".DisableAutoFallback").on("change", function() {
saveProxyInlineEdit(uuid);
});