Networking

How to generate random IPv6 addresses in a given network using Python

This code generates random IPv6 addresses in a given network using Python’s ipaddress module:

import ipaddress
import random

def random_ipv6_addr(network):
    """
    Generate a random IPv6 address in the given network
    Example: random_ipv6_addr("fd66:6cbb:8c10::/48")
    Returns an IPv6Address object.
    """
    net = ipaddress.IPv6Network(network)
    # Which of the network.num_addresses we want to select?
    addr_no = random.randint(0, net.num_addresses)
    # Create the random address by converting to a 128-bit integer, adding addr_no and converting back
    network_int = int.from_bytes(net.network_address.packed, byteorder="big")
    addr_int = network_int + addr_no
    addr = ipaddress.IPv6Address(addr_int.to_bytes(16, byteorder="big"))
    return addr

# Usage example
print(random_ipv6_addr("fdce:4879:a1e9::/48"))
# Prints e.g. fdce:4879:a1e9:e351:1a01:be9:4d9a:157d

It works by first converting the IPv6 network address to binary and then adding a random host number. After that, it will be converted back to an IPv6Address object.

Posted by Uli Köhler in Networking, Python

How to get hostmask/netmask for given prefix length in Python

In order to get the host mask for e.g. a /112 IPv6 prefix, use:

import ipaddress
# Get netmask for a /112 prefix
ipaddress.IPv6Network("::/112").netmask

# Get host mask for a /112 prefix
ipaddress.IPv6Network("::/112").hostmask

 

Posted by Uli Köhler in Networking, Python

Bitwise operation with IPv6 addresses and networks in Python

Python3 features the easy-to-use ipaddress library providing many calculations. However, bitwise boolean operators are not available for addresses.

This post shows you how to perform bitwise operations with IPv6Address() objects. We’ll use the following strategy:

  1. Use .packed to get a binary bytes() instance of the IP address
  2. Use int.from_bytes() to acquire an integer representing the binary address
  3. Perform bitwise operations with said integer
  4. Use result.to_bytes(16, ...) to convert back the integer to a bytes() array in the correct byte order
  5. Construct an IPv6Address() object from the resulting byte array.

Python code:

import ipaddress

def bitwise_and_ipv6(addr1, addr2):
    result_int = int.from_bytes(addr1.packed, byteorder="big") & int.from_bytes(addr2.packed, byteorder="big")
    return ipaddress.IPv6Address(result_int.to_bytes(16, byteorder="big"))

def bitwise_or_ipv6(addr1, addr2):
    result_int = int.from_bytes(addr1.packed, byteorder="big") | int.from_bytes(addr2.packed, byteorder="big")
    return ipaddress.IPv6Address(result_int.to_bytes(16, byteorder="big"))

def bitwise_xor_ipv6(addr1, addr2):
    result_int = int.from_bytes(addr1.packed, byteorder="big") ^ int.from_bytes(addr2.packed, byteorder="big")
    return ipaddress.IPv6Address(result_int.to_bytes(16, byteorder="big"))

Example usage:

a = ipaddress.IPv6Address('2001:16b8:2703:8835:9ec7:a6ff:febe:96b1')
b = ipaddress.IPv6Address('2001:16b8:2703:4241:9ec7:a6ff:febe:96b1')

print(bitwise_and_ipv6(a, b)) # IPv6Address('2001:16b8:2703:1:9ec7:a6ff:febe:96b1')
print(bitwise_or_ipv6(a, b)) # IPv6Address('2001:16b8:2703:ca75:9ec7:a6ff:febe:96b1')
print(bitwise_xor_ipv6(a, b)) # IPv6Address('0:0:0:ca74::')

Similarly, you can use the code in order to manipulate IPv6Network() instances:

a = ipaddress.IPv6Network('2001:16b8:2703:8835:9ec7:a6ff:febe::/112')
b = ipaddress.IPv6Network('2001:16b8:2703:4241:9ec7:a6ff:febe::/112')

print(bitwise_and_ipv6(a.network_address, b.network_address)) # IPv6Address('2001:16b8:2703:1:9ec7:a6ff:febe:0')
print(bitwise_or_ipv6(a.network_address, b.network_address)) # IPv6Address('2001:16b8:2703:ca75:9ec7:a6ff:febe:0')
print(bitwise_xor_ipv6(a.network_address, b.network_address)) # IPv6Address('0:0:0:ca74::')

Note that the return type will always be IPv6Address() and never IPv6Network() since the result of the bitwise operation doesn’t have any netmask associated with it.

Besides .network_address you can also use other properties of IPv6Address() instances like .broadcast_address or .hostmask or .netmask.

Posted by Uli Köhler in Networking, Python

How to fix OpenVPN “TLS Error: cannot locate HMAC in incoming packet from …”

Problem:

Your OpenVPN clients can’t connect to your OpenVPN server and the server log shows an error message like

TLS Error: cannot locate HMAC in incoming packet from [AF_INET6]::ffff:187.100.14.13:41874 (via ::ffff:25.16.25.29%xn0)

Solution:

You have enabled a TLS key (tls-auth option) in your OpenVPN configuration, but your client does not know that it should use the additional layer of authentication.

The server is looking for the HMAC in the incoming packets but can’t find it.

Either disable the tls-auth option in your server config. The config line will look like

tls-auth /var/etc/openvpn/server2.tls-auth 0

or

Enable the correct tls-auth configuration in your client. Remember that you also need to share the correct key.

Posted by Uli Köhler in Networking, OpenVPN, VPN

How to setup OnlyOffice using docker-compose & nginx

Prerequisite: Install docker and docker-compose

For example, follow our guide How to install docker and docker-compose on Ubuntu in 30 seconds

Step 1: Create docker-compose.yml

Create the directory where we’ll install OnlyOffice using

sudo mkdir /var/lib/onlyoffice

and then edit the docker-compose configuration using e.g.

sudo nano /var/lib/onlyoffice/docker-compose.yml

and copy and paste this content

version: '3'
services:
  onlyoffice-documentserver:
    image: onlyoffice/documentserver:latest
    restart: always
    environment:
      - JWT_ENABLED=true
      - JWT_SECRET=ahSaTh4waeKe4zoocohngaihaub5pu
    ports:
      - 2291:80
    volumes:
      - ./onlyoffice/data:/var/www/onlyoffice/Data
      - ./onlyoffice/lib:/var/lib/onlyoffice
      - ./onlyoffice/logs:/var/log/onlyoffice
      - ./onlyoffice/db:/var/lib/postgresql

Now add your custom password in JWT_SECRET=... ! Don’t forget this step, or anyone can use your OnlyOffice server ! I’m using pwgen 30 to generate a new random password (install using sudo apt -y install pwgen).

Step 2: Setup systemd service

Create the service using sudo nano /etc/systemd/system/onlyoffice.service:

[Unit]
Description=OnlyOffice server
Requires=docker.service
After=docker.service

[Service]
Restart=always
User=root
Group=docker
# Shutdown container (if running) when unit is stopped
ExecStartPre=/usr/local/bin/docker-compose -f /var/lib/onlyoffice/docker-compose.yml down -v
# Start container when unit is started
ExecStart=/usr/local/bin/docker-compose -f /var/lib/onlyoffice/docker-compose.yml up
# Stop container when unit is stopped
ExecStop=/usr/local/bin/docker-compose -f /var/lib/onlyoffice/docker-compose.yml down -v

[Install]
WantedBy=multi-user.target

Now enable & start the service using

sudo systemctl enable onlyoffice
sudo systemctl start onlyoffice

Step 3:  Create nginx reverse proxy configuration

Note that we mapped OnlyOffice’s port 80 to port 2291. In case you’re not using nginx as reverse proxy, you need to manually configure your reverse proxy to pass requests to port 2291.

server {
    server_name onlyoffice.mydomain.org;

    access_log /var/log/nginx/onlyoffice.access_log;
    error_log /var/log/nginx/onlyoffice.error_log info;

    location / {
        proxy_pass http://127.0.0.1:2291;
        proxy_http_version 1.1;
        proxy_read_timeout 3600s;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host            $host;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        add_header X-Frontend-Host $host;
        # Uncomment this line and reload once you have setup TLS for that domain !
        # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    }

    listen 80;
}

Now test if your nginx config works using nginx -t and reload using service nginx reload.

Now I recommend to setup Let’s Encrypt for your domain so that your OnlyOffice instance will only be accessed using an encrypted connecting (sudo certbot --nginx, see other guides if you don’t know how to do that).

Once certbot asks you whether to redirect, choose option 2 – Redirect to HTTPS.

Step 4: Test OnlyOffice

If your installation worked, you should see a screen like this:

If not, try checking the logs using

sudo journalctl -xu onlyoffice

(Optional) Step 5: Configure NextCloud to use OnlyOffice

If you are running NextCloud, go to Settings => ONLYOFFICE and enter your domain and the JWT_SECRET you created before:

Ensure that Connect to demo ONLYOFFICE Document Server is unchecked and click Save.

Nextcloud will tell you at the top right if it has been able to connect to your OnlyOffice instance successfully:

  • Settings successfully updated means that NextCloud is now connected to OnlyOffice
  • Invalid token means that your password / secret key does not match
  • Other messages typically mean that your OnlyOffice is not running or that you haven’t entered the correct domain or protocol. I recommend to only use https:// – use http:// for testing only and don’t forget to revert back to https:// once you have found the issue.
Posted by Uli Köhler in Container, Docker, Linux, nginx

How to fix NextCloud OnlyOffice MixedContent or ‘Refused to frame ‘http://…’ because it violates the following Content Security Policy directive: “frame-src https://…”.

Problem:

In reverse-proxy setups  forwarding requests to OnlyOffice like our reference setup there you might encounter issues like

Refused to frame 'http://onlyoffice.mydomain.com/' because it violates the following Content Security Policy directive: "frame-src https://onlyoffice.mydomain.com/".

Solution:

Just add

proxy_set_header X-Forwarded-Proto $scheme;

directly after your proxy_pass clause in your nginx config, then run sudo service nginx reload.

The reason for this issue is that OnlyOffice thinks it’s being loaded using HTTP, but the Nextcloud page prevents insecure content from being loaded.

Using a proxy other than nginx? Just ensure that every proxied request (i.e. every request directed towards the OnlyOffice instance) has the X-Forwarded-Proto header set to the protocol of the original request – which should be https.

Posted by Uli Köhler in Nextcloud, nginx

How to setup OnlyOffice using docker-compose, systemd and nginx

In this setup we show how to setup OnlyOffice using nginx as a reverse proxy, docker-compose to run and configure the OnlyOffice image and systemd to automatically start and restart the OnlyOffice instance. Running it in a reverse proxy configuration allows you to have other domains listening on the same IP address and have a central management of Let’s Encrypt SSL certificates.

We will setup the instance in /opt/onlyoffice on port 2291.

Save this file as /opt/onlyoffice/docker-compose.yml and don’t forget to change JWT_SECRET to a random password!

version: '3'
services:
  onlyoffice-documentserver:
    image: onlyoffice/documentserver:latest
    restart: always
    environment:
      - JWT_ENABLED=true
      - JWT_SECRET=Shei9AifuZ4ze7udahG2seb3aa6ool
    ports:
      - 2291:80
    volumes:
      - ./onlyoffice/data:/var/www/onlyoffice/Data
      - ./onlyoffice/lib:/var/lib/onlyoffice
      - ./onlyoffice/logs:/var/log/onlyoffice
      - ./onlyoffice/db:/var/lib/postgresql

Now we can create the systemd service. I created it using TechOverflow’s docker-compose systemd .service generator. Save it in /etc/systemd/system/OnlyOffice.service:

[Unit]
Description=OnlyOffice
Requires=docker.service
After=docker.service

[Service]
Restart=always
User=root
Group=docker
# Shutdown container (if running) when unit is stopped
ExecStartPre=/usr/local/bin/docker-compose -f /opt/onlyoffice/docker-compose.yml down
# Start container when unit is started
ExecStart=/usr/local/bin/docker-compose -f /opt/onlyoffice/docker-compose.yml up
# Stop container when unit is stopped
ExecStop=/usr/local/bin/docker-compose -f /opt/onlyoffice/docker-compose.yml down

[Install]
WantedBy=multi-user.target

Now we can enable & start the service using

sudo systemctl enable OnlyOffice.service
sudo systemctl start OnlyOffice.service

Now let’s create the nginx config in /etc/nginx/sites-enabled/OnlyOffice.conf. Obviously, you’ll have to modify at least the

 server {
    server_name onlyoffice.mydomain.com;

    access_log /var/log/nginx/onlyoffice.access_log;
    error_log /var/log/nginx/onlyoffice.error_log info;

    location / {
        proxy_pass http://127.0.0.1:2291;
        proxy_http_version 1.1;
        proxy_read_timeout 3600s;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        add_header X-Frontend-Host $host;
    }

    listen 80;
}

Check the validity of the nginx config using

sudo nginx -t

and unless it fails, reload nginx using

sudo service nginx reload

Now I recommend to use certbot to enable TLS encryption on your domain. You should be familiar with these steps already ; my approach is to sudo apt -y install python-certbot-nginx, then certbot --nginx --staging to first obtain a staging certificate to avoid being blocked if there are any issues and after you have obtained the staging certificate use certbot --nginx and Renew & replace cert. After that, run sudo service nginx reload and check if you domain works with HTTPS. You should always choose redirection to HTTPS if certbot asks you.

Posted by Uli Köhler in Docker, nginx

Auto-generate nginx forwarding configs using this script

A major hassle for me when setting up lots of docker-based services on a machine is to setup the individual nginx configs to forward the domains to the correct services.

TechOverflow provides a simple way to automatically generate nginx configs for a single domain to configure port forwarding to a specific port.

wget -qO- https://techoverflow.net/scripts/generate-nginx-docker-config.sh | sudo bash /dev/stdin service.mydomain.com 1234

Note: This script was tested on Ubuntu 18.04 and is regularly used by myself and others. However, if used incorrectly or in case there is a major bug, it might damage your webserver configuration, so be sure to be prepared to fix any issues that might arise. This script is provided without any warranty, expressed or implied.

Remember to replace service.mydomain.com by your domain and 1234 by the local port your docker service is listening to.

The script will generate /etc/nginx/sites-enabled/[domain].conf.

Run

sudo nginx -t

to check if the config syntax is OK, and only if that succeeds, run

sudo service nginx reload

Now your domain should be online and I recommend running

sudo certbot --nginx

 

Posted by Uli Köhler in nginx

How to fix nginx FastCGI error ‘upstream sent too big header while reading response header from upstream’

Problem:

You’re getting 502 Bad gateway errors in your nginx + FastCGI (PHP) setup. You see error messages like

2020/01/28 11:58:19 [error] 9728#9728: *1 upstream sent too big header while reading response header from upstream, client: 2001:16b8:2681:7600:bc28:b49d:3318:e9c4, server: techoverflow.net, request: "GET /category/calculators/ HTTP/2.0", upstream: "fastcgi://unix:/var/run/php/php7.2-fpm.sock:", host: "techoverflow.net", referrer: "https://techoverflow.net/?s=calcul"

in your error log.

Solution:

You need to increase your FastCGI buffers by adding

fastcgi_buffers 32 256k;
fastcgi_buffer_size 512k;

next to every instance of fastcgi_pass in your nginx config and then restarting nginx:

sudo service nginx restart

Note that the values for the buffer sizes listed in this example are just recommendations and might be adjusted up or down depending on your requirements – however, these values tend to work well for modern server hardware (although many administrators tend to use smaller buffers).

Posted by Uli Köhler in nginx

How to get external IPv4/IPv6 address in Go

You can use IPIfy’s HTTP API within Go:

import (
    "io/ioutil"
    "net/http"
)

func GetExternalIPv4Address() (string, error) {
    // Perform request
    resp, err := http.Get("https://api4.ipify.org")
    if err != nil {
        return "", err
    }
    // Cleanup when this function ends
    defer resp.Body.Close()
    // Read all the response data into a []byte
    body, err := ioutil.ReadAll(resp.Body)
    // Decode & print
    return string(body), nil
}

func GetExternalIPv6Address() (string, error) {
    // Perform request
    resp, err := http.Get("https://api6.ipify.org")
    if err != nil {
        return "", err
    }
    // Cleanup when this function ends
    defer resp.Body.Close()
    // Read all the response data into a []byte
    body, err := ioutil.ReadAll(resp.Body)
    // Decode & print
    return string(body), nil
}

Usage example:

package main

func main() {
    v4, _ := GetExternalIPv4Address()
    v6, _ := GetExternalIPv6Address()
    println(v4)
    println(v6)
}

If you save both files in a directory named GoIPAddress, you can run

$ go build
$ ./GOIPAddress
31.190.168.110
2a03:4012:2:1022::1

 

 

Posted by Uli Köhler in Go, Networking

How to stop vpnc IPSec VPNs and close the tunnel interface

Problem:

You have started a VPN connection using vpnc, e.g.

$ sudo vpnc my-vpn.conf
VPNC started in background (pid: 21763)...

but you can’t find any information on how to stop vpnc i.e. terminating the VPN connection.

Solution:

Preferred method: Use vpnc-disconnect

Run

sudo vpnc-disconnect

This will, for example, print

Terminating vpnc daemon (pid: 21763)

vpnc-disconnect is the official method of stopping vpnc and will terminate the vpnc instance whose PID is written in /var/run/vpnc.pid. In other words, it will not work properly if you have multiple vpnc instances running at the same time, or if you have specified an alternate PID file for vpnc (e.g. using vpnc --pid-file /var/run/my-vpnc.pid my-vpn.conf).

Alternate method 1: Stop all vpnc instances on the current machine

You can kill all vpnc instances on the current machine using

sudo killall vpnc

however this will stop all vpnc instances on the current machine. In case you have multiple vpnc VPN instances active concurrently, this means all of them will be terminated.

Alternate method 2: Kill a specific vpnc (if you know it’s PID)

vpnc tells you its process ID when starting it. In our example above:

VPNC started in background (pid: 21763)...

the PID is 21763 so we can kill the process using

sudo kill 21763

This will cleanly stop vpnc and remove the tunnel interface.

Alternate method 3: Kill a specific vpnc (if you don’t know it’s PID)

Show all running vpnc instances using

pgrep -a vpnc

This will show you, for example,

21763 vpnc my-vpn.conf
30792 vpnc other-vpn.conf

In that list, find the line with the vpnc instance you want to kill (you can identify it by the config file name, e.g. my-vpn.conf – in this example, it would be the first line).

The number at the beginning of the line is the PID of that vpnc process. Copy it and run

sudo kill [PID]

e.g.

sudo kill 21763

just like in Alternate method 2. This will only stop that specific vpnc instance and leave all others running.

Posted by Uli Köhler in Networking

How to set Access-Control-Allow-Origin * in nginx

To allow access from all origins, use

add_header 'Access-Control-Allow-Origin' '*' always;

You might want to add

add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';

as well.

Posted by Uli Köhler in nginx

How to make your own ‘get my current IP address’ server using only nginx

Note: In some configurations, this returns e.g. ::ffff:1.2.3.4 for IPv4 requests and I did not resolve this yet

This nginx config will return the user’s IP address to the user. Note that it won’t work behind a reverse proxy, it actually needs to listen on port 80 and/or 443 of the IP address of the domain it is served as.

location = /api/get-my-ip {
    add_header 'Access-Control-Allow-Origin' '*' always;
    add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
    add_header 'Content-Type' 'text/plain charset=UTF-8';
    return 200 '$remote_addr';
}

This is a JSON-returning variant:

location = /api/get-my-ip-json {
    add_header 'Access-Control-Allow-Origin' '*' always;
    add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
    add_header 'Content-Type' 'text/plain charset=UTF-8';
    return 200 '{"ip": "$remote_addr"}';
}

This configuration is available online, for free, at techoverflow.net: See How to get your current IPv4 address using wget and How to get your current IPv6 address using wget

Posted by Uli Köhler in Networking, nginx

How to fix nginx rewrite redirect causing certbot verification errors

When using a redirection to a new domain using rewrite in my nginx config, I had issues with Let’s Encrypt not being able to verify the domain ownership when using certbot --nginx.

My redirect statement was

location / {
    rewrite ^/$ https://newdomain.com permanent;
    rewrite ^/(.*)$ https://newdomain.com/$1 permanent;
}

I was able to fix the issue by instead using this redirect statement:

location / {
     return 301 https://newdomain.com$request_uri;
}

After using this, I was able to use certbot --nginx without any issues.

Posted by Uli Köhler in nginx

How to redirect to a new domain using nginx

In order to redirect to a different domain using nginx, use this snippet inside your server {...} block:

location / {
     return 301 https://newdomain.com$request_uri;
}

Full example:

server {
    server_name old.domain;
    listen 80;

    location / {
         return 301 https://techoverflow.net$request_uri;
    }
}
Posted by Uli Köhler in nginx

nginx: Where to get $request_basename from

nginx by default does not provide a $request_basename, but you can define it yourself using a regex named group

location ~ ^/(?P<request_basename>[^/]+)$ {
    # TODO: Add your code here - you can use $request_basename!
}

For a usage example, see my previous post on nginx: Force file download using Content-Disposition for entire directory.

Posted by Uli Köhler in nginx

How to extract only filename (basename) in nginx

You can easily extract only the filename (e.g. myfile.txt for https://domain.com/directory/myfile.txt) by capturing it in a regex named group:

location ~ ^/(?P<request_basename>[^/]+)$ {
    # TODO: Add your code here - you can use $request_basename!
}

For a usage example, see my previous post on nginx: Force file download using Content-Disposition for entire directory.

Posted by Uli Köhler in nginx

nginx: Force file download using Content-Disposition for entire directory

To force downloading all files in a directory using nginx by using the Content-Disposition header, use this snippet:

# Force files in /scripts to download instead of display!
location ~ ^/scripts/(?P<request_basename>[^/]+)$ {
    add_header Content-Disposition "attachment; filename=$request_basename";
}

Note that this might cause issues with some mobile iOS devices which do not use a proper filesystem.

Original source: coderwall.com (but heavily modified)

Posted by Uli Köhler in nginx

How to install automated certbot/LetsEncrypt renewal in 30 seconds

Let’s Encrypt currently issues certificates for 3 months at a time only. For many users, this mandates automated renewal of Let’s Encrypt certificates, however many manuals how to install automated renewals on ordinary Linux servers are needlessly complicated.

I created a systemd-timer based daily renewal routine using TechOverflow’s Simple systemd timer generator.

Quick install using

wget -qO- https://techoverflow.net/scripts/install-renew-certbot.sh | sudo bash

This is the script which automatically creates & installs both systemd config files.

#!/bin/sh
# This script installs automated certbot renewal onto systemd-based systems.
# It requires that certbot is installed in /usr/bin/certbot!
# This needs to be run using sudo!

cat >/etc/systemd/system/RenewCertbot.service <<EOF
[Unit]
Description=RenewCertbot

[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew
WorkingDirectory=/tmp
EOF

cat >/etc/systemd/system/RenewCertbot.timer <<EOF
[Unit]
Description=RenewCertbot

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target
EOF

# Enable and start service
systemctl enable RenewCertbot.timer && sudo systemctl start RenewCertbot.timer

To view logs, use

journalctl -xfu RenewCertbot.service

To view the status, use

sudo systemctl status RenewCertbot.timer

To immediately run a renewal, use

sudo systemctl start RenewCertbot.service
Posted by Uli Köhler in Linux, nginx

Minimal nginx WebSocket reverse-proxy example

If you have a nginx config clause like

location /app/ {
    proxy_pass http://app-server;
}

you can easily make it reverse-proxy Websocket connections as well by adding these clauses inside location { ... }:

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";

Full example:

location /app/ {
    proxy_pass http://app-server;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
}

 

Posted by Uli Köhler in nginx