Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import app from "./app.js";
import internalCertificate from "./internal/certificate.js";
import internalIpRanges from "./internal/ip_ranges.js";
import internalNginx from "./internal/nginx.js";
import { global as logger } from "./logger.js";
import { migrateUp } from "./migrate.js";
import { getCompiledSchema } from "./schema/index.js";
Expand All @@ -14,6 +15,9 @@ async function appStart() {
return migrateUp()
.then(setup)
.then(getCompiledSchema)
.then(() => {
logger.info(`Upstream resolver: ${internalNginx.getUpstreamResolver()}`);
})
.then(() => {
if (!IP_RANGES_FETCH_ENABLED) {
logger.info("IP Ranges fetch is disabled by environment variable");
Expand Down
29 changes: 29 additions & 0 deletions backend/internal/nginx.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ const internalNginx = {
{ http2_support: host.http2_support },
{ hsts_enabled: host.hsts_enabled },
{ hsts_subdomains: host.hsts_subdomains },
{ dynamic_upstream_resolve: host.dynamic_upstream_resolve },
{ upstream_resolver: host.upstream_resolver },
{ access_list: host.access_list },
{ certificate: host.certificate },
host.locations[i],
Expand Down Expand Up @@ -241,6 +243,9 @@ const internalNginx = {
// Set the IPv6 setting for the host
host.ipv6 = internalNginx.ipv6Enabled();

// Set the upstream resolver (used when dynamic_upstream_resolve is enabled)
host.upstream_resolver = internalNginx.getUpstreamResolver();

locationsPromise.then(() => {
renderEngine
.parseAndRender(template, host)
Expand Down Expand Up @@ -431,6 +436,30 @@ const internalNginx = {
}

return true;
},

/**
* Returns the DNS resolver address to use in nginx `resolver` directives.
* Priority: NGINX_RESOLVER env var → first nameserver in /etc/resolv.conf → 127.0.0.11
*
* @returns {String}
*/
getUpstreamResolver: () => {
if (process.env.NGINX_RESOLVER) {
return process.env.NGINX_RESOLVER;
}

try {
const resolvConf = fs.readFileSync("/etc/resolv.conf", { encoding: "utf8" });
const match = resolvConf.match(/^\s*nameserver\s+(\S+)/m);
if (match) {
return match[1];
}
} catch (_err) {
// ignore — fall through to default
}

return "127.0.0.11";
},
};

Expand Down
4 changes: 2 additions & 2 deletions backend/migrations/20260131163528_trust_forwarded_proto.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const migrateName = "trust_forwarded_proto";
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
const up = function (knex) {
logger.info(`[${migrateName}] Migrating Up...`);

return knex.schema
Expand All @@ -28,7 +28,7 @@ const up = (knex) => {
* @param {Object} knex
* @returns {Promise}
*/
const down = (knex) => {
const down = function (knex) {
logger.info(`[${migrateName}] Migrating Down...`);

return knex.schema
Expand Down
43 changes: 43 additions & 0 deletions backend/migrations/20260414000000_dynamic_upstream_resolve.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { migrate as logger } from "../logger.js";

const migrateName = "dynamic_upstream_resolve";

/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);

return knex.schema
.alterTable('proxy_host', (table) => {
table.tinyint('dynamic_upstream_resolve').notNullable().defaultTo(0);
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`);
});
};

/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);

return knex.schema
.alterTable('proxy_host', (table) => {
table.dropColumn('dynamic_upstream_resolve');
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`);
});
};

export { up, down };
1 change: 1 addition & 0 deletions backend/models/proxy_host.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const boolFields = [
"enabled",
"hsts_enabled",
"hsts_subdomains",
"dynamic_upstream_resolve",
"trust_forwarded_proto",
];

Expand Down
6 changes: 6 additions & 0 deletions backend/schema/components/proxy-host-object.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"enabled",
"locations",
"hsts_enabled",
"dynamic_upstream_resolve",
"hsts_subdomains",
"trust_forwarded_proto"
],
Expand Down Expand Up @@ -147,6 +148,11 @@
"description": "Trust the forwarded headers",
"example": false
},
"dynamic_upstream_resolve": {
"type": "boolean",
"description": "Resolve upstream host dynamically using resolver directive",
"example": false
},
"certificate": {
"oneOf": [
{
Expand Down
1 change: 1 addition & 0 deletions backend/schema/paths/nginx/proxy-hosts/get.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"nginx_err": null
},
"allow_websocket_upgrade": false,
"dynamic_upstream_resolve": false,
"http2_support": false,
"forward_scheme": "http",
"enabled": true,
Expand Down
1 change: 1 addition & 0 deletions backend/schema/paths/nginx/proxy-hosts/hostID/get.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"nginx_err": null
},
"allow_websocket_upgrade": false,
"dynamic_upstream_resolve": false,
"http2_support": false,
"forward_scheme": "http",
"enabled": true,
Expand Down
4 changes: 4 additions & 0 deletions backend/schema/paths/nginx/proxy-hosts/hostID/put.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
"trust_forwarded_proto": {
"$ref": "../../../../components/proxy-host-object.json#/properties/trust_forwarded_proto"
},
"dynamic_upstream_resolve": {
"$ref": "../../../../components/proxy-host-object.json#/properties/dynamic_upstream_resolve"
},
"http2_support": {
"$ref": "../../../../components/proxy-host-object.json#/properties/http2_support"
},
Expand Down Expand Up @@ -119,6 +122,7 @@
"nginx_err": null
},
"allow_websocket_upgrade": false,
"dynamic_upstream_resolve": false,
"http2_support": false,
"forward_scheme": "http",
"enabled": true,
Expand Down
4 changes: 4 additions & 0 deletions backend/schema/paths/nginx/proxy-hosts/post.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
"trust_forwarded_proto": {
"$ref": "../../../components/proxy-host-object.json#/properties/trust_forwarded_proto"
},
"dynamic_upstream_resolve": {
"$ref": "../../../components/proxy-host-object.json#/properties/dynamic_upstream_resolve"
},
"http2_support": {
"$ref": "../../../components/proxy-host-object.json#/properties/http2_support"
},
Expand Down Expand Up @@ -116,6 +119,7 @@
"advanced_config": "",
"meta": {},
"allow_websocket_upgrade": false,
"dynamic_upstream_resolve": false,
"http2_support": false,
"forward_scheme": "http",
"enabled": true,
Expand Down
5 changes: 5 additions & 0 deletions backend/templates/_location.conf
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;

{% if dynamic_upstream_resolve == 1 or dynamic_upstream_resolve == true %}
set $upstream_host "{{ forward_host }}";
proxy_pass {{ forward_scheme }}://$upstream_host:{{ forward_port }}{{ forward_path }};
{% else %}
proxy_pass {{ forward_scheme }}://{{ forward_host }}:{{ forward_port }}{{ forward_path }};
{% endif %}

{% include "_access.conf" %}
{% include "_assets.conf" %}
Expand Down
4 changes: 4 additions & 0 deletions backend/templates/proxy_host.conf
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ server {
set $server "{{ forward_host }}";
set $port {{ forward_port }};

{% if dynamic_upstream_resolve == 1 or dynamic_upstream_resolve == true %}
resolver {{ upstream_resolver }} valid=10s;
{% endif %}

{% include "_listen.conf" %}
{% include "_certificates.conf" %}
{% include "_assets.conf" %}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/api/backend/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export interface ProxyHost {
hstsEnabled: boolean;
hstsSubdomains: boolean;
trustForwardedProto: boolean;
dynamicUpstreamResolve: boolean;
// Expansions:
owner?: User;
accessList?: AccessList;
Expand Down
1 change: 1 addition & 0 deletions frontend/src/hooks/useProxyHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const fetchProxyHost = (id: number | "new") => {
hstsEnabled: false,
hstsSubdomains: false,
trustForwardedProto: false,
dynamicUpstreamResolve: false,
} as ProxyHost);
}
return getProxyHost(id, ["owner"]);
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/locale/src/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,12 @@
"host.flags.websockets-upgrade": {
"defaultMessage": "Websockets Support"
},
"host.flags.dynamic-upstream-resolve": {
"defaultMessage": "Dynamic Upstream Resolve"
},
"host.flags.dynamic-upstream-resolve-description": {
"defaultMessage": "Resolves upstream hostnames dynamically at request time. The DNS resolver is auto-detected from /etc/resolv.conf, or can be overridden with the NGINX_RESOLVER environment variable. Defaults to 127.0.0.11 (Docker bridge DNS) if no resolver is found."
},
"host.forward-port": {
"defaultMessage": "Forward Port"
},
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/locale/src/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,12 @@
"host.flags.websockets-upgrade": {
"defaultMessage": "Prise en charge de Websockets"
},
"host.flags.dynamic-upstream-resolve": {
"defaultMessage": "Résolution dynamique de l'upstream"
},
"host.flags.dynamic-upstream-resolve-description": {
"defaultMessage": "Résout les noms d'hôtes upstream dynamiquement à chaque requête. Le résolveur DNS est détecté automatiquement depuis /etc/resolv.conf, ou peut être remplacé via la variable d'environnement NGINX_RESOLVER. Par défaut : 127.0.0.11 (DNS Docker bridge) si aucun résolveur n'est trouvé."
},
"host.forward-port": {
"defaultMessage": "Port de redirection"
},
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/modals/ProxyHostModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
hstsEnabled: data?.hstsEnabled || false,
hstsSubdomains: data?.hstsSubdomains || false,
trustForwardedProto: data?.trustForwardedProto || false,
dynamicUpstreamResolve: data?.dynamicUpstreamResolve || false,
// Advanced tab
advancedConfig: data?.advancedConfig || "",
meta: data?.meta || {},
Expand Down Expand Up @@ -328,6 +329,32 @@ const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
</span>
</label>
</div>
<div>
<label className="row" htmlFor="dynamicUpstreamResolve">
<span className="col">
<T id="host.flags.dynamic-upstream-resolve" />
<div className="small text-muted mt-1">
<T id="host.flags.dynamic-upstream-resolve-description" />
</div>
</span>
<span className="col-auto">
<Field name="dynamicUpstreamResolve" type="checkbox">
{({ field }: any) => (
<label className="form-check form-check-single form-switch">
<input
{...field}
id="dynamicUpstreamResolve"
className={cn("form-check-input", {
"bg-lime": field.checked,
})}
type="checkbox"
/>
</label>
)}
</Field>
</span>
</label>
</div>
</div>
</div>
</div>
Expand Down
52 changes: 43 additions & 9 deletions test/cypress/e2e/api/ProxyHosts.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,16 @@ describe('Proxy Hosts endpoints', () => {
meta: {
dns_challenge: false
},
advanced_config: '',
locations: [],
block_exploits: false,
caching_enabled: false,
allow_websocket_upgrade: false,
http2_support: false,
hsts_enabled: false,
hsts_subdomains: false,
ssl_forced: false
advanced_config: '',
locations: [],
block_exploits: false,
caching_enabled: false,
allow_websocket_upgrade: false,
http2_support: false,
hsts_enabled: false,
hsts_subdomains: false,
ssl_forced: false,
dynamic_upstream_resolve: false,
}
}).then((data) => {
cy.validateSwaggerSchema('post', 201, '/nginx/proxy-hosts', data);
Expand All @@ -45,4 +46,37 @@ describe('Proxy Hosts endpoints', () => {
});
});

it('Should be able to create a proxy host with dynamic upstream resolve enabled', () => {
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/proxy-hosts',
data: {
domain_names: ['dynamic-resolve.example.com'],
forward_scheme: 'http',
forward_host: 'my.node',
forward_port: 8080,
access_list_id: '0',
certificate_id: 0,
meta: {
dns_challenge: false
},
advanced_config: '',
locations: [],
block_exploits: false,
caching_enabled: false,
allow_websocket_upgrade: false,
http2_support: false,
hsts_enabled: false,
hsts_subdomains: false,
ssl_forced: false,
dynamic_upstream_resolve: true,
}
}).then((data) => {
cy.validateSwaggerSchema('post', 201, '/nginx/proxy-hosts', data);
expect(data).to.have.property('id');
expect(data.id).to.be.greaterThan(0);
expect(data).to.have.property('dynamic_upstream_resolve', true);
});
});

});