Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93cf034e61 | |||
| 2102b309de | |||
| c3fe25335f | |||
| 4321826dc8 |
@@ -83,6 +83,7 @@ RUN apt-get update \
|
|||||||
# resty.limit.req is bundled with OpenResty, so no rock is needed for it.
|
# resty.limit.req is bundled with OpenResty, so no rock is needed for it.
|
||||||
RUN luarocks install lua-resty-auto-ssl \
|
RUN luarocks install lua-resty-auto-ssl \
|
||||||
&& luarocks install luasocket \
|
&& luarocks install luasocket \
|
||||||
|
&& luarocks install lua-resty-balancer \
|
||||||
&& luarocks install lua-resty-ipmatcher
|
&& luarocks install lua-resty-ipmatcher
|
||||||
|
|
||||||
# ── Node app ─────────────────────────────────────────────────────────────────
|
# ── Node app ─────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ provider + LDAP directory you already run.
|
|||||||
- Multiple DNS provider integrations (Cloudflare, DigitalOcean, PorkBun, DuckDNS — DuckDNS is free)
|
- Multiple DNS provider integrations (Cloudflare, DigitalOcean, PorkBun, DuckDNS — DuckDNS is free)
|
||||||
- Wildcard SSL certificate support with automatic renewal
|
- Wildcard SSL certificate support with automatic renewal
|
||||||
- Dynamic host routing with wildcard domain matching (*, **)
|
- Dynamic host routing with wildcard domain matching (*, **)
|
||||||
|
- **Multi-target load balancing** — configure multiple backend targets per host with built-in round-robin load balancing
|
||||||
- Web-based management interface
|
- Web-based management interface
|
||||||
- RESTful API for automation
|
- RESTful API for automation
|
||||||
- **OIDC login** — the proxy is an OpenID Connect client of an external SSO
|
- **OIDC login** — the proxy is an OpenID Connect client of an external SSO
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ host form whenever the name you're entering already has a matching
|
|||||||
wildcard available to reuse — including the wildcard's own bare base
|
wildcard available to reuse — including the wildcard's own bare base
|
||||||
domain (e.g. `example.com` itself, not just `something.example.com`).
|
domain (e.g. `example.com` itself, not just `something.example.com`).
|
||||||
|
|
||||||
|
## Load Balancing
|
||||||
|
|
||||||
|
If you have multiple servers running the same application, you can load balance traffic across them. When editing a host, you can specify **Additional Targets** (one `IP:port` per line). The proxy will automatically distribute incoming requests across your primary target and all additional targets using a round-robin strategy, providing simple high availability and load distribution without extra configuration.
|
||||||
|
|
||||||
## Want more detail?
|
## Want more detail?
|
||||||
|
|
||||||
This page skips the system-internals (Redis, OpenResty, the lookup service)
|
This page skips the system-internals (Redis, OpenResty, the lookup service)
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ LDAP directory you already run.
|
|||||||
- Automated HTTPS via Let's Encrypt — HTTP-01 and DNS-01 (wildcard) challenges
|
- Automated HTTPS via Let's Encrypt — HTTP-01 and DNS-01 (wildcard) challenges
|
||||||
- Multiple DNS providers (Cloudflare, DigitalOcean, PorkBun, DuckDNS — free)
|
- Multiple DNS providers (Cloudflare, DigitalOcean, PorkBun, DuckDNS — free)
|
||||||
- Dynamic host routing with wildcard domain matching (`*`, `**`)
|
- Dynamic host routing with wildcard domain matching (`*`, `**`)
|
||||||
|
- **Multi-target load balancing** — configure multiple backend targets per host with built-in round-robin load balancing
|
||||||
- **OIDC login** and **direct LDAP lookups**, independently of each other
|
- **OIDC login** and **direct LDAP lookups**, independently of each other
|
||||||
- Per-host **basic auth** as an alternative to SSO (mutually exclusive, so
|
- Per-host **basic auth** as an alternative to SSO (mutually exclusive, so
|
||||||
it's never ambiguous which one gated a request)
|
it's never ambiguous which one gated a request)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class Host extends Table{
|
|||||||
'host': {isRequired: true, type: 'string', min: 1, max: 500},
|
'host': {isRequired: true, type: 'string', min: 1, max: 500},
|
||||||
'ip': {isRequired: true, type: 'string', min: 3, max: 500},
|
'ip': {isRequired: true, type: 'string', min: 3, max: 500},
|
||||||
'targetPort': {isRequired: true, type: 'number', min:0, max:65535},
|
'targetPort': {isRequired: true, type: 'number', min:0, max:65535},
|
||||||
|
'targets': {default: function(){return []}, isRequired: false, type: 'object'},
|
||||||
'forcessl': {isRequired: false, default: true, type: 'boolean'},
|
'forcessl': {isRequired: false, default: true, type: 'boolean'},
|
||||||
'targetssl': {isRequired: false, default: false, type: 'boolean'},
|
'targetssl': {isRequired: false, default: false, type: 'boolean'},
|
||||||
|
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ function normalizeHostFeatures(body){
|
|||||||
if('sso_enabled' in body) body.sso_enabled = toBool(body.sso_enabled);
|
if('sso_enabled' in body) body.sso_enabled = toBool(body.sso_enabled);
|
||||||
if('sso_allow_users' in body) body.sso_allow_users = parseAllowList(body.sso_allow_users);
|
if('sso_allow_users' in body) body.sso_allow_users = parseAllowList(body.sso_allow_users);
|
||||||
if('sso_allow_groups' in body) body.sso_allow_groups = parseAllowList(body.sso_allow_groups);
|
if('sso_allow_groups' in body) body.sso_allow_groups = parseAllowList(body.sso_allow_groups);
|
||||||
|
if('targets' in body) body.targets = parseAllowList(body.targets);
|
||||||
|
|
||||||
if('ratelimit_rate' in body) body.ratelimit_rate = clampNumber(body.ratelimit_rate, 1, 1000000, 10);
|
if('ratelimit_rate' in body) body.ratelimit_rate = clampNumber(body.ratelimit_rate, 1, 1000000, 10);
|
||||||
if('ratelimit_burst' in body) body.ratelimit_burst = clampNumber(body.ratelimit_burst, 0, 1000000, 20);
|
if('ratelimit_burst' in body) body.ratelimit_burst = clampNumber(body.ratelimit_burst, 0, 1000000, 20);
|
||||||
|
|||||||
@@ -232,6 +232,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$f.find("textarea[name='req_headers']").val(hostFeatureHeadersToText(h.req_headers));
|
$f.find("textarea[name='req_headers']").val(hostFeatureHeadersToText(h.req_headers));
|
||||||
|
$f.find("textarea[name='targets']").val(hostFeatureListToText(h.targets));
|
||||||
$f.find("textarea[name='resp_headers']").val(hostFeatureHeadersToText(h.resp_headers));
|
$f.find("textarea[name='resp_headers']").val(hostFeatureHeadersToText(h.resp_headers));
|
||||||
$f.find("textarea[name='ip_allow']").val(hostFeatureListToText(h.ip_allow));
|
$f.find("textarea[name='ip_allow']").val(hostFeatureListToText(h.ip_allow));
|
||||||
$f.find("textarea[name='ip_deny']").val(hostFeatureListToText(h.ip_deny));
|
$f.find("textarea[name='ip_deny']").val(hostFeatureListToText(h.ip_deny));
|
||||||
@@ -632,6 +633,14 @@
|
|||||||
<small class="field-help text-muted d-block">Whether the proxy talks to the target over HTTP or HTTPS. Independent of Incoming SSL above — clients can use HTTPS to reach the proxy while it still talks plain HTTP to the target, or vice versa.</small>
|
<small class="field-help text-muted d-block">Whether the proxy talks to the target over HTTP or HTTPS. Independent of Incoming SSL above — clients can use HTTPS to reach the proxy while it still talks plain HTTP to the target, or vice versa.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="targets" class="form-label">Additional Targets (Load Balancing)</label>
|
||||||
|
<textarea name="targets" class="form-control" rows="2" placeholder="10.0.0.2:8080 10.0.0.3:8080"></textarea>
|
||||||
|
<small class="field-help text-muted d-block">Add additional targets here (IP:port, one per line) to load balance across them using round-robin. The primary target above is always included.</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TLS & Wildcard -->
|
<!-- TLS & Wildcard -->
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ apt-get install -y nodejs openresty
|
|||||||
echo "==> Lua modules"
|
echo "==> Lua modules"
|
||||||
luarocks install lua-resty-auto-ssl
|
luarocks install lua-resty-auto-ssl
|
||||||
luarocks install luasocket
|
luarocks install luasocket
|
||||||
|
luarocks install lua-resty-balancer
|
||||||
# CIDR matcher for the per-host IP allow/deny lists (hostfeatures.lua).
|
# CIDR matcher for the per-host IP allow/deny lists (hostfeatures.lua).
|
||||||
# resty.limit.req is bundled with OpenResty, so no rock is needed for it.
|
# resty.limit.req is bundled with OpenResty, so no rock is needed for it.
|
||||||
luarocks install lua-resty-ipmatcher
|
luarocks install lua-resty-ipmatcher
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ function M.get(ngx, domain, targetInfo)
|
|||||||
|
|
||||||
local json = require "cjson"
|
local json = require "cjson"
|
||||||
local redis = require "resty.redis"
|
local redis = require "resty.redis"
|
||||||
|
local round_robin = require "resty.balancer.round_robin"
|
||||||
|
|
||||||
if not domain then
|
if not domain then
|
||||||
return nil, 499
|
return nil, 499
|
||||||
@@ -95,6 +96,45 @@ function M.get(ngx, domain, targetInfo)
|
|||||||
return nil, 406
|
return nil, 406
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Load balancing
|
||||||
|
local target_list = {}
|
||||||
|
table.insert(target_list, res["ip"] .. ":" .. tostring(res["targetPort"]))
|
||||||
|
|
||||||
|
if res["targets"] and res["targets"] ~= "" and res["targets"] ~= "[]" then
|
||||||
|
local decodeOk, decodedTargets = pcall(json.decode, res["targets"])
|
||||||
|
if decodeOk and type(decodedTargets) == "table" then
|
||||||
|
for _, t in ipairs(decodedTargets) do
|
||||||
|
table.insert(target_list, t)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #target_list > 1 then
|
||||||
|
if not M.host_balancers then M.host_balancers = {} end
|
||||||
|
local cache_key = domain .. "_" .. (res["updated_on"] or "0")
|
||||||
|
|
||||||
|
if not M.host_balancers[domain] or M.host_balancers[domain].key ~= cache_key then
|
||||||
|
local b = round_robin:new()
|
||||||
|
local nodes = {}
|
||||||
|
for _, t in ipairs(target_list) do
|
||||||
|
nodes[t] = 1
|
||||||
|
end
|
||||||
|
b:reinit(nodes)
|
||||||
|
M.host_balancers[domain] = { b = b, key = cache_key }
|
||||||
|
end
|
||||||
|
|
||||||
|
local peer = M.host_balancers[domain].b:find()
|
||||||
|
if peer then
|
||||||
|
local colon = peer:find(":")
|
||||||
|
if colon then
|
||||||
|
res["ip"] = peer:sub(1, colon - 1)
|
||||||
|
res["targetPort"] = peer:sub(colon + 1)
|
||||||
|
else
|
||||||
|
res["ip"] = peer
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
ngx.ctx.targetInfo = res
|
ngx.ctx.targetInfo = res
|
||||||
-- Remember which host this target was resolved for, so the reuse guard at
|
-- Remember which host this target was resolved for, so the reuse guard at
|
||||||
-- the top can tell a genuine cache hit from a coalesced request for a
|
-- the top can tell a genuine cache hit from a coalesced request for a
|
||||||
|
|||||||
Reference in New Issue
Block a user