diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js index fe84607f96..86a0a77051 100644 --- a/backend/internal/nginx.js +++ b/backend/internal/nginx.js @@ -160,6 +160,7 @@ const internalNginx = { { http2_support: host.http2_support }, { hsts_enabled: host.hsts_enabled }, { hsts_subdomains: host.hsts_subdomains }, + { dynamic_upstream_resolve: host.dynamic_upstream_resolve }, { access_list: host.access_list }, { certificate: host.certificate }, host.locations[i], diff --git a/backend/migrations/20260131163528_trust_forwarded_proto.js b/backend/migrations/20260131163528_trust_forwarded_proto.js index 546cbca674..c32c6fb697 100644 --- a/backend/migrations/20260131163528_trust_forwarded_proto.js +++ b/backend/migrations/20260131163528_trust_forwarded_proto.js @@ -10,7 +10,7 @@ const migrateName = "trust_forwarded_proto"; * @param {Object} knex * @returns {Promise} */ -const up = function (knex) { +const up = (knex) => { logger.info(`[${migrateName}] Migrating Up...`); return knex.schema @@ -28,7 +28,7 @@ const up = function (knex) { * @param {Object} knex * @returns {Promise} */ -const down = function (knex) { +const down = (knex) => { logger.info(`[${migrateName}] Migrating Down...`); return knex.schema diff --git a/backend/migrations/20260414000000_dynamic_upstream_resolve.js b/backend/migrations/20260414000000_dynamic_upstream_resolve.js new file mode 100644 index 0000000000..1847faad89 --- /dev/null +++ b/backend/migrations/20260414000000_dynamic_upstream_resolve.js @@ -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 }; diff --git a/backend/models/proxy_host.js b/backend/models/proxy_host.js index acb8da9358..6b3507f5fc 100644 --- a/backend/models/proxy_host.js +++ b/backend/models/proxy_host.js @@ -21,6 +21,7 @@ const boolFields = [ "enabled", "hsts_enabled", "hsts_subdomains", + "dynamic_upstream_resolve", "trust_forwarded_proto", ]; diff --git a/backend/schema/components/proxy-host-object.json b/backend/schema/components/proxy-host-object.json index 3ac6462136..10b06b24c7 100644 --- a/backend/schema/components/proxy-host-object.json +++ b/backend/schema/components/proxy-host-object.json @@ -22,6 +22,7 @@ "enabled", "locations", "hsts_enabled", + "dynamic_upstream_resolve", "hsts_subdomains", "trust_forwarded_proto" ], @@ -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": [ { diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/put.json b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json index fc3198456b..163bdab1c0 100644 --- a/backend/schema/paths/nginx/proxy-hosts/hostID/put.json +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json @@ -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" }, diff --git a/backend/schema/paths/nginx/proxy-hosts/post.json b/backend/schema/paths/nginx/proxy-hosts/post.json index 28ddad8fc2..c3b2126389 100644 --- a/backend/schema/paths/nginx/proxy-hosts/post.json +++ b/backend/schema/paths/nginx/proxy-hosts/post.json @@ -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" }, diff --git a/backend/templates/_location.conf b/backend/templates/_location.conf index a2ecb166d6..60b3a2cfb2 100644 --- a/backend/templates/_location.conf +++ b/backend/templates/_location.conf @@ -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" %} diff --git a/backend/templates/proxy_host.conf b/backend/templates/proxy_host.conf index d23ca46fa2..fa3e103674 100644 --- a/backend/templates/proxy_host.conf +++ b/backend/templates/proxy_host.conf @@ -9,6 +9,10 @@ server { set $server "{{ forward_host }}"; set $port {{ forward_port }}; +{% if dynamic_upstream_resolve == 1 or dynamic_upstream_resolve == true %} + resolver 127.0.0.11 valid=10s; +{% endif %} + {% include "_listen.conf" %} {% include "_certificates.conf" %} {% include "_assets.conf" %} diff --git a/frontend/src/api/backend/models.ts b/frontend/src/api/backend/models.ts index 2ae0b08348..02e04eab5b 100644 --- a/frontend/src/api/backend/models.ts +++ b/frontend/src/api/backend/models.ts @@ -128,6 +128,7 @@ export interface ProxyHost { hstsEnabled: boolean; hstsSubdomains: boolean; trustForwardedProto: boolean; + dynamicUpstreamResolve: boolean; // Expansions: owner?: User; accessList?: AccessList; diff --git a/frontend/src/hooks/useProxyHost.ts b/frontend/src/hooks/useProxyHost.ts index 24e7f4fae2..2808d3abef 100644 --- a/frontend/src/hooks/useProxyHost.ts +++ b/frontend/src/hooks/useProxyHost.ts @@ -25,6 +25,7 @@ const fetchProxyHost = (id: number | "new") => { hstsEnabled: false, hstsSubdomains: false, trustForwardedProto: false, + dynamicUpstreamResolve: false, } as ProxyHost); } return getProxyHost(id, ["owner"]); diff --git a/frontend/src/locale/src/en.json b/frontend/src/locale/src/en.json index bb00ac3322..2bd9a9f4b7 100644 --- a/frontend/src/locale/src/en.json +++ b/frontend/src/locale/src/en.json @@ -437,6 +437,9 @@ "host.flags.websockets-upgrade": { "defaultMessage": "Websockets Support" }, + "host.flags.dynamic-upstream-resolve": { + "defaultMessage": "Dynamic Upstream Resolve" + }, "host.forward-port": { "defaultMessage": "Forward Port" }, diff --git a/frontend/src/locale/src/fr.json b/frontend/src/locale/src/fr.json index c715c028a6..56a03a9008 100644 --- a/frontend/src/locale/src/fr.json +++ b/frontend/src/locale/src/fr.json @@ -347,6 +347,9 @@ "host.flags.websockets-upgrade": { "defaultMessage": "Prise en charge de Websockets" }, + "host.flags.dynamic-upstream-resolve": { + "defaultMessage": "Résolution dynamique de l'Upstream" + }, "host.forward-port": { "defaultMessage": "Port de redirection" }, diff --git a/frontend/src/modals/ProxyHostModal.tsx b/frontend/src/modals/ProxyHostModal.tsx index 3227be51bb..8b37c0626a 100644 --- a/frontend/src/modals/ProxyHostModal.tsx +++ b/frontend/src/modals/ProxyHostModal.tsx @@ -328,6 +328,29 @@ const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => { +
+ +
diff --git a/test/cypress/e2e/api/ProxyHosts.cy.js b/test/cypress/e2e/api/ProxyHosts.cy.js index 5f437cf950..dba29414ce 100644 --- a/test/cypress/e2e/api/ProxyHosts.cy.js +++ b/test/cypress/e2e/api/ProxyHosts.cy.js @@ -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); @@ -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); + }); + }); + });