diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js index 5f802c004..b847222a5 100644 --- a/backend/internal/nginx.js +++ b/backend/internal/nginx.js @@ -156,7 +156,8 @@ const internalNginx = { {ssl_forced: host.ssl_forced}, {caching_enabled: host.caching_enabled}, {block_exploits: host.block_exploits}, {allow_websocket_upgrade: host.allow_websocket_upgrade}, {http2_support: host.http2_support}, {hsts_enabled: host.hsts_enabled}, {hsts_subdomains: host.hsts_subdomains}, {access_list: host.access_list}, - {certificate: host.certificate}, host.locations[i]); + {certificate: host.certificate}, {proxy_protocol_enabled: host.proxy_protocol_enabled}, + {loadbalancer_address: host.loadbalancer_address}, host.locations[i]); if (locationCopy.forward_host.indexOf('/') > -1) { const splitted = locationCopy.forward_host.split('/'); diff --git a/backend/migrations/20241022221324_proxy_protocol.js b/backend/migrations/20241022221324_proxy_protocol.js new file mode 100644 index 000000000..fa1bd196e --- /dev/null +++ b/backend/migrations/20241022221324_proxy_protocol.js @@ -0,0 +1,56 @@ +const migrate_name = 'proxy_protocol'; +const logger = require('../logger').migrate; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.up = function (knex/*, Promise*/) { + logger.info('[' + migrate_name + '] Migrating Up...'); + + return knex.schema.table('proxy_host', function (proxy_host) { + proxy_host.integer('proxy_protocol_enabled').notNull().defaultTo(0); + proxy_host.string('loadbalancer_address').notNull().defaultTo(''); + }) + .then(() => { + logger.info('[' + migrate_name + '] proxy_host Table altered'); + + return knex.schema.table('stream', function (stream) { + stream.integer('proxy_protocol_enabled').notNull().defaultTo(0); + stream.string('loadbalancer_address').notNull().defaultTo(''); + }) + .then(() => { + logger.info('[' + migrate_name + '] stream Table altered'); + }); + }); + +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.down = function (knex/*, Promise*/) { + return knex.schema.table('proxy_host', function (proxy_host) { + proxy_host.dropColumn('proxy_protocol_enabled'); + proxy_host.dropColumn('loadbalancer_address'); + }) + .then(function () { + logger.info('[' + migrate_name + '] proxy_host Table altered'); + return knex.schema.table('stream', function (stream) { + stream.dropColumn('proxy_protocol_enabled'); + stream.dropColumn('loadbalancer_address'); + }) + .then(function () { + logger.info('[' + migrate_name + '] stream Table altered'); + }); + }); +}; \ No newline at end of file diff --git a/backend/models/proxy_host.js b/backend/models/proxy_host.js index 07aa5dd3c..d45acf02c 100644 --- a/backend/models/proxy_host.js +++ b/backend/models/proxy_host.js @@ -21,6 +21,7 @@ const boolFields = [ 'enabled', 'hsts_enabled', 'hsts_subdomains', + 'proxy_protocol_enabled', ]; class ProxyHost extends Model { diff --git a/backend/models/stream.js b/backend/models/stream.js index b96ca5a17..efc18f0ad 100644 --- a/backend/models/stream.js +++ b/backend/models/stream.js @@ -13,6 +13,7 @@ const boolFields = [ 'is_deleted', 'tcp_forwarding', 'udp_forwarding', + 'proxy_protocol_enabled', ]; class Stream extends Model { diff --git a/backend/schema/common.json b/backend/schema/common.json index 83de0143c..dee65224f 100644 --- a/backend/schema/common.json +++ b/backend/schema/common.json @@ -110,6 +110,16 @@ "caching_enabled": { "description": "Should we cache assets", "type": "boolean" + }, + "proxy_protocol_enabled": { + "description": "Should the proxy_procotol be enabled", + "type": "boolean" + }, + "loadbalancer_address": { + "description": "Hostname, IP or CIDR range of the load balancer", + "type": "string", + "minLength": 0, + "maxLength": 255 } } } diff --git a/backend/schema/components/proxy-host-object.json b/backend/schema/components/proxy-host-object.json index 5098802b1..195c85a48 100644 --- a/backend/schema/components/proxy-host-object.json +++ b/backend/schema/components/proxy-host-object.json @@ -23,7 +23,9 @@ "locations", "hsts_enabled", "hsts_subdomains", - "certificate" + "certificate", + "proxy_protocol_enabled", + "loadbalancer_address" ], "additionalProperties": false, "properties": { @@ -137,6 +139,12 @@ } ] }, + "proxy_protocol_enabled": { + "$ref": "../common.json#/properties/proxy_protocol_enabled" + }, + "loadbalancer_address": { + "$ref": "../common.json#/properties/loadbalancer_address" + }, "owner": { "$ref": "./user-object.json" }, diff --git a/backend/schema/components/stream-object.json b/backend/schema/components/stream-object.json index 516c7f891..0d087c155 100644 --- a/backend/schema/components/stream-object.json +++ b/backend/schema/components/stream-object.json @@ -1,7 +1,7 @@ { "type": "object", "description": "Stream object", - "required": ["id", "created_on", "modified_on", "owner_user_id", "incoming_port", "forwarding_host", "forwarding_port", "tcp_forwarding", "udp_forwarding", "enabled", "meta"], + "required": ["id", "created_on", "modified_on", "owner_user_id", "incoming_port", "forwarding_host", "forwarding_port", "tcp_forwarding", "udp_forwarding", "enabled", "meta", "proxy_protocol_enabled", "loadbalancer_address"], "additionalProperties": false, "properties": { "id": { @@ -55,6 +55,12 @@ }, "meta": { "type": "object" + }, + "proxy_protocol_enabled": { + "$ref": "../common.json#/properties/proxy_protocol_enabled" + }, + "loadbalancer_address": { + "$ref": "../common.json#/properties/loadbalancer_address" } } } diff --git a/backend/schema/paths/nginx/proxy-hosts/get.json b/backend/schema/paths/nginx/proxy-hosts/get.json index 1d9f63351..64e1b1877 100644 --- a/backend/schema/paths/nginx/proxy-hosts/get.json +++ b/backend/schema/paths/nginx/proxy-hosts/get.json @@ -50,7 +50,9 @@ "enabled": true, "locations": null, "hsts_enabled": false, - "hsts_subdomains": false + "hsts_subdomains": false, + "proxy_protocol_enabled": false, + "loadbalancer_address": "" } ] } diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/get.json b/backend/schema/paths/nginx/proxy-hosts/hostID/get.json index 5e10a9cfd..6d0562e32 100644 --- a/backend/schema/paths/nginx/proxy-hosts/hostID/get.json +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/get.json @@ -50,7 +50,9 @@ "enabled": true, "locations": null, "hsts_enabled": false, - "hsts_subdomains": false + "hsts_subdomains": false, + "proxy_protocol_enabled": false, + "loadbalancer_address": "" } } }, diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/put.json b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json index 5cab6e752..997e6ff1a 100644 --- a/backend/schema/paths/nginx/proxy-hosts/hostID/put.json +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json @@ -79,6 +79,12 @@ }, "locations": { "$ref": "../../../../components/proxy-host-object.json#/properties/locations" + }, + "proxy_protocol_enabled": { + "$ref": "../../../../components/proxy-host-object.json#/properties/proxy_protocol_enabled" + }, + "loadbalancer_address": { + "$ref": "../../../../components/proxy-host-object.json#/properties/loadbalancer_address" } } } @@ -116,6 +122,8 @@ "enabled": true, "hsts_enabled": false, "hsts_subdomains": false, + "proxy_protocol_enabled": false, + "loadbalancer_address": "", "owner": { "id": 1, "created_on": "2024-10-07T22:43:55.000Z", diff --git a/backend/schema/paths/nginx/proxy-hosts/post.json b/backend/schema/paths/nginx/proxy-hosts/post.json index 85455fb6b..340731a47 100644 --- a/backend/schema/paths/nginx/proxy-hosts/post.json +++ b/backend/schema/paths/nginx/proxy-hosts/post.json @@ -67,6 +67,12 @@ }, "locations": { "$ref": "../../../components/proxy-host-object.json#/properties/locations" + }, + "proxy_protocol_enabled": { + "$ref": "../../../components/proxy-host-object.json#/properties/proxy_protocol_enabled" + }, + "loadbalancer_address": { + "$ref": "../../../components/proxy-host-object.json#/properties/loadbalancer_address" } } } @@ -101,6 +107,8 @@ "enabled": true, "hsts_enabled": false, "hsts_subdomains": false, + "proxy_protocol_enabled": false, + "loadbalancer_address": "", "certificate": null, "owner": { "id": 1, diff --git a/backend/schema/paths/nginx/streams/get.json b/backend/schema/paths/nginx/streams/get.json index 596afc6e7..04c68a3b6 100644 --- a/backend/schema/paths/nginx/streams/get.json +++ b/backend/schema/paths/nginx/streams/get.json @@ -36,6 +36,8 @@ "forwarding_port": 80, "tcp_forwarding": true, "udp_forwarding": false, + "proxy_protocol_enabled": false, + "loadbalancer_address": "", "meta": { "nginx_online": true, "nginx_err": null diff --git a/backend/schema/paths/nginx/streams/post.json b/backend/schema/paths/nginx/streams/post.json index 9f3514e0f..02a46a0c5 100644 --- a/backend/schema/paths/nginx/streams/post.json +++ b/backend/schema/paths/nginx/streams/post.json @@ -32,6 +32,12 @@ "udp_forwarding": { "$ref": "../../../components/stream-object.json#/properties/udp_forwarding" }, + "proxy_protocol_enabled": { + "$ref": "../../../components/stream-object.json#/properties/proxy_protocol_enabled" + }, + "loadbalancer_address": { + "$ref": "../../../components/stream-object.json#/properties/loadbalancer_address" + }, "meta": { "$ref": "../../../components/stream-object.json#/properties/meta" } @@ -57,6 +63,8 @@ "forwarding_port": 80, "tcp_forwarding": true, "udp_forwarding": false, + "proxy_protocol_enabled": false, + "loadbalancer_address": "", "meta": { "nginx_online": true, "nginx_err": null diff --git a/backend/schema/paths/nginx/streams/streamID/get.json b/backend/schema/paths/nginx/streams/streamID/get.json index 6547656df..9329def07 100644 --- a/backend/schema/paths/nginx/streams/streamID/get.json +++ b/backend/schema/paths/nginx/streams/streamID/get.json @@ -36,6 +36,8 @@ "forwarding_port": 80, "tcp_forwarding": true, "udp_forwarding": false, + "proxy_protocol_enabled": false, + "loadbalancer_address": "", "meta": { "nginx_online": true, "nginx_err": null diff --git a/backend/schema/paths/nginx/streams/streamID/put.json b/backend/schema/paths/nginx/streams/streamID/put.json index fbfdc901b..ac2cd8856 100644 --- a/backend/schema/paths/nginx/streams/streamID/put.json +++ b/backend/schema/paths/nginx/streams/streamID/put.json @@ -79,6 +79,12 @@ }, "locations": { "$ref": "../../../../components/proxy-host-object.json#/properties/locations" + }, + "proxy_protocol_enabled": { + "$ref": "../../../../components/proxy-host-object.json#/properties/proxy_protocol_enabled" + }, + "loadbalancer_address": { + "$ref": "../../../../components/proxy-host-object.json#/properties/loadbalancer_address" } } } @@ -116,6 +122,8 @@ "enabled": true, "hsts_enabled": false, "hsts_subdomains": false, + "proxy_protocol_enabled": false, + "loadbalancer_address": "", "owner": { "id": 1, "created_on": "2024-10-07T22:43:55.000Z", diff --git a/backend/templates/_listen.conf b/backend/templates/_listen.conf index 34a808e6a..45bd156a1 100644 --- a/backend/templates/_listen.conf +++ b/backend/templates/_listen.conf @@ -1,20 +1,34 @@ - listen 80; + +{% if proxy_protocol_enabled == 1 or proxy_protocol_enabled == true -%} +{% assign port_number_http = "88" -%} +{% assign port_number_https = "444" -%} +{% assign listen_extra_args = "proxy_protocol" -%} +{% else -%} +{% assign port_number_http = "80" -%} +{% assign port_number_https = "443" -%} +{% assign listen_extra_args = "" -%} +{% endif -%} + + listen {{ port_number_http }} {{ listen_extra_args }}; {% if ipv6 -%} - listen [::]:80; + listen [::]:{{ port_number_http }} {{ listen_extra_args }}; {% else -%} - #listen [::]:80; -{% endif %} + #listen [::]:{{ port_number_http }} {{ listen_extra_args }}; +{% endif -%} + {% if certificate -%} - listen 443 ssl; +{% capture listen_extra_args_https %}ssl {{ listen_extra_args }}{% endcapture -%} + listen {{ port_number_https }} {{ listen_extra_args_https }}; {% if ipv6 -%} - listen [::]:443 ssl; + listen [::]:{{ port_number_https }} {{ listen_extra_args_https }}; {% else -%} - #listen [::]:443; -{% endif %} -{% endif %} + #listen [::]:{{ port_number_https }} {{ listen_extra_args_https }}; +{% endif -%} +{% endif -%} + server_name {{ domain_names | join: " " }}; {% if http2_support == 1 or http2_support == true %} http2 on; {% else -%} http2 off; -{% endif %} \ No newline at end of file +{% endif %} diff --git a/backend/templates/_proxy_protocol.conf b/backend/templates/_proxy_protocol.conf new file mode 100644 index 000000000..96d615c18 --- /dev/null +++ b/backend/templates/_proxy_protocol.conf @@ -0,0 +1,6 @@ +{% if proxy_protocol_enabled == 1 or proxy_protocol_enabled == true %} +{% if loadbalancer_address != '' %} + set_real_ip_from {{ loadbalancer_address }}; + real_ip_header proxy_protocol; +{% endif %} +{% endif %} \ No newline at end of file diff --git a/backend/templates/proxy_host.conf b/backend/templates/proxy_host.conf index d23ca46fa..e753b6dde 100644 --- a/backend/templates/proxy_host.conf +++ b/backend/templates/proxy_host.conf @@ -15,6 +15,7 @@ server { {% include "_exploits.conf" %} {% include "_hsts.conf" %} {% include "_forced_ssl.conf" %} +{% include "_proxy_protocol.conf" %} {% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %} proxy_set_header Upgrade $http_upgrade; diff --git a/backend/templates/stream.conf b/backend/templates/stream.conf index 76159a646..72b7242da 100644 --- a/backend/templates/stream.conf +++ b/backend/templates/stream.conf @@ -1,31 +1,38 @@ # ------------------------------------------------------------ # {{ incoming_port }} TCP: {{ tcp_forwarding }} UDP: {{ udp_forwarding }} # ------------------------------------------------------------ +{% if proxy_protocol_enabled == 1 or proxy_protocol_enabled == true -%} +{% capture listen_extra_args %}proxy_protocol{% endcapture -%} +{% endif -%} {% if enabled %} {% if tcp_forwarding == 1 or tcp_forwarding == true -%} server { - listen {{ incoming_port }}; + listen {{ incoming_port }} {{ listen_extra_args }}; {% if ipv6 -%} - listen [::]:{{ incoming_port }}; + listen [::]:{{ incoming_port }} {{ listen_extra_args }}; {% else -%} - #listen [::]:{{ incoming_port }}; + #listen [::]:{{ incoming_port }}{{ listen_extra_args }}; {% endif %} proxy_pass {{ forwarding_host }}:{{ forwarding_port }}; +{% include '_proxy_protocol.conf' %} + # Custom include /data/nginx/custom/server_stream[.]conf; include /data/nginx/custom/server_stream_tcp[.]conf; } {% endif %} {% if udp_forwarding == 1 or udp_forwarding == true %} +{% # Proxy Protocol is not supported for UDP %} +{% assign listen_extra_args = "" %} server { - listen {{ incoming_port }} udp; + listen {{ incoming_port }} udp {{ listen_extra_args }}; {% if ipv6 -%} - listen [::]:{{ incoming_port }} udp; + listen [::]:{{ incoming_port }} udp {{ listen_extra_args }}; {% else -%} - #listen [::]:{{ incoming_port }} udp; + #listen [::]:{{ incoming_port }} udp {{ listen_extra_args }}; {% endif %} proxy_pass {{ forwarding_host }}:{{ forwarding_port }}; diff --git a/docker/Dockerfile b/docker/Dockerfile index 0603e2ded..ad84aa077 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -35,7 +35,8 @@ RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \ COPY docker/scripts/install-s6 /tmp/install-s6 RUN /tmp/install-s6 "${TARGETPLATFORM}" && rm -f /tmp/install-s6 -EXPOSE 80 81 443 +# http admin_ui http_proxy_protocol https https_proxy_protocol +EXPOSE 80 81 88 443 444 COPY backend /app COPY frontend/dist /app/frontend diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index bb4ac6d44..5b89e863c 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -35,5 +35,6 @@ RUN rm -f /etc/nginx/conf.d/production.conf \ COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt -EXPOSE 80 81 443 +# http admin_ui http_proxy_protocol https https_proxy_protocol +EXPOSE 80 81 88 443 444 ENTRYPOINT [ "/init" ] diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 2bfa2b798..d7a8b5638 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -10,7 +10,9 @@ services: ports: - 3080:80 - 3081:81 + - 3088:88 - 3443:443 + - 3444:444 networks: nginx_proxy_manager: aliases: diff --git a/docs/src/advanced-config/index.md b/docs/src/advanced-config/index.md index efeaefec3..82e183a18 100644 --- a/docs/src/advanced-config/index.md +++ b/docs/src/advanced-config/index.md @@ -222,3 +222,27 @@ To enable the geoip2 module, you can create the custom configuration file `/data load_module /usr/lib/nginx/modules/ngx_http_geoip2_module.so; load_module /usr/lib/nginx/modules/ngx_stream_geoip2_module.so; ``` + +## Enabling PROXY protocol for Proxy Hosts + +When running NPM behind a load balancer, you might want to use the [PROXY procotol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) to receive client information such as the source IP address (useful for banning IPs). + +When configuring the PROXY protocol for proxy hosts, NPM uses the ports 88 for http and 444 for https traffic to allow you to decide on a per host basis whether to use the PROSY protocol. + +To enable the PROXY protocol for your hosts you need to perform the following steps: + +1. Expose the ports `88` (and `444` is applicable) by adjusting your `docker-compose.yml` +2. Edit your proxy hosts to enable the PROXY protocol +3. Edit your upstream load balancer to redirect traffic to the port `88`/`444` and enable the PROXY protocol + +## Enabling PROXY protocol for Streams + +When running NPM behind a load balancer, you might want to use the [PROXY procotol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) to receive client information such as the source IP address (useful for banning IPs). + +Keep in mind that the PROXY procotol cannot be enabled for udp endpoints. + +To enable the PROXY protocol for streams: + +1. Expose the desired port by adjusting you `docker-compose.yml` +2. Edit the Stream to enable the PROXY protocol +3. Edit your upstream load balancer to enable the PROXY protocol diff --git a/docs/src/setup/index.md b/docs/src/setup/index.md index ee8e9903d..6dd58418c 100644 --- a/docs/src/setup/index.md +++ b/docs/src/setup/index.md @@ -19,6 +19,8 @@ services: - '80:80' # Public HTTP Port - '443:443' # Public HTTPS Port - '81:81' # Admin Web Port + # - '88:88' # Public HTTP Port with proxy_protocol enabled + # - '444:444' # Public HTTPS Port with proxy_protocol enabled # Add any other Stream port you want to expose # - '21:21' # FTP diff --git a/frontend/js/app/nginx/proxy/form.ejs b/frontend/js/app/nginx/proxy/form.ejs index 8e7a2a2df..33b4c682f 100644 --- a/frontend/js/app/nginx/proxy/form.ejs +++ b/frontend/js/app/nginx/proxy/form.ejs @@ -72,7 +72,7 @@ -