Category: Networking

  • Firewalls and DDNS, Part 2

    As discussed in the Firewalls and DDNS post, DuckDNS can sometime be slow. Additionally, some corporate DNS resolves do not resolve the DuckDNS domains making them inaccessible. To solve this, I decided to use Cloudflare’s name servers to handle my domains. Contrary to the spirit of free open source software, when it comes to domain registration, it is not free. You may be able to find deals on domain name registration, but they’re usually promotional and will eventually make you pay. I went with Cloudflare and their domain registration services for simplicity and a single control plane since I decided I’d be using their DNS.

    Creating a DDNS automation script

    Create a folder in your home folder names something like .ddns, like so mkdir -p ~/.ddns. Create and edit a file called variables in this .ddns folder with your favorite text editor, I prefer nano, nano ~/.ddns/variables.

    In the variables add your (you guessed it) variables you got from Cloudflare.

    SUBDOMAIN="<subdomain.fdqn.tld>"
    KEY="<key>"
    ZONE_ID="<zone_id>"

    You need to protect this file. This file contains information that will allow changing your DNS records in your Cloudflare account.

    chmod 500 ~/.ddns
    cd ~/.ddns
    chmod 400 ~/.ddns/variables
    chmod 500 ~/.ddns/ddns.sh
    #!/bin/bash
    
    # This script updates the Cloudflare's A DNS records for your domain.
    # It requires the DNS edit token key and the DNS Zone ID.
    # The script uses curl for the API calls and awk parse the responses.
    # Be sure to create the subdomain A DNS record in the Cloudflare portal with a dummy routable IP address first.
    # Create a file in the same folder where you place this script and name the file variables
    # Put these lines in to the file variables
    #SUBDOMAIN="<subdomain.fdqn.tld>"
    #KEY="<key>"
    #ZONE_ID="<zone_id>"
    # Make sure the user who will be executing this script can read the file
    
    # Function to validate IP address
    validate_ip() {
        local ip=$1
    
        # Regular expression to match valid IPv4 addresses
        local valid_ip_regex="^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$"
    
        # Regular expressions to exclude non-routable IP addresses
        local non_routable_regexes=(
            "^0\.([0-9]{1,3}\.){2}[0-9]{1,3}$"
            "^10\.([0-9]{1,3}\.){2}[0-9]{1,3}$"
            "^100\.(6[4-9]|7[0-9]|1[0-1][0-9]|12[0-7])\.([0-9]{1,3}\.)[0-9]{1,3}$"
            "^127\.([0-9]{1,3}\.){2}[0-9]{1,3}$"
            "^169\.254\.([0-9]{1,3}\.)[0-9]{1,3}$"
            "^172\.(1[6-9]|2[0-9]|3[0-1])\.([0-9]{1,3}\.)[0-9]{1,3}$"
            "^192\.0\.0\.([0-9]{1,3})$"
            "^192\.0\.2\.([0-9]{1,3})$"
            "^192\.88\.99\.([0-9]{1,3})$"
            "^192\.168\.([0-9]{1,3}\.)[0-9]{1,3}$"
            "^198\.(1[8-9])\.([0-9]{1,3}\.)[0-9]{1,3}$"
            "^198\.51\.100\.([0-9]{1,3})$"
            "^203\.0\.113\.([0-9]{1,3})$"
            "^224\.([0-9]{1,3}\.){2}[0-9]{1,3}$"
            "^(24[0-9]|25[0-5])\.([0-9]{1,3}\.){2}[0-9]{1,3}$"
        )
    
        # Check if the IP address matches the valid IP regex
        if [[ $ip =~ $valid_ip_regex ]]; then
            # Check if the IP address matches any of the non-routable IP regexes
            for regex in "${non_routable_regexes[@]}"; do
                if [[ $ip =~ $regex ]]; then
                    echo "Invalid IP address: Non-routable IP address"
                    return 1
                fi
            done
            echo "$ip is a valid IP address"
            return 0
        else
            echo "Invalid IP address: Does not match IPv4 format"
            return 1
        fi
    }
    
    # Make sure that the ddns.sh and variables files are in the same directory
    
    source ./variables
    
    # Get old IP address
    OLD_IP=$(host $SUBDOMAIN | awk '/has address/ { print $4 }')
    if validate_ip "$OLD_IP"; then
      echo "Old IP for $SUBDOMAIN is $OLD_IP"
    else
      echo "Failed getting old IP"
      exit 1
    fi
    
    # Get new IP address
    NEW_IP=$(curl -s --connect-timeout 20 https://checkip.amazonaws.com)
    if validate_ip "$NEW_IP"; then
      echo "New IP for $SUBDOMAIN is $NEW_IP"
    else
      echo "Failed getting new IP"
      exit 1
    fi
    
    # Compare the IP addresses
    if [ "$NEW_IP" == "$OLD_IP" ]; then
        echo "IP is already correct"
    else
    
    # Get Subdomain DNS Record from Cloudflare
    DNS_RECORD_ID=$(curl -s --connect-timeout 20 --request GET \
        --url https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records \
        --header 'Content-Type: application/json' \
        --header "Authorization: Bearer $KEY" \
            | awk -v RS='{"' -F: '/^id/ && /'"$SUBDOMAIN"'/{print $2}' | tr -d '"' | sed 's/,.*//')
    echo "Got Record ID"
    
    # Update IP Address
    curl -s -o /dev/null --connect-timeout 20 --request PUT \
      --url https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$DNS_RECORD_ID \
      --header 'Content-Type: application/json' \
      --header "Authorization: Bearer $KEY" \
      --data "{\"type\":\"A\",\"name\":\"$SUBDOMAIN\",\"content\":\"$NEW_IP\",\"ttl\":1,\"proxied\":false}" 
    echo "IP address for $SUBDOMAIN is updated to $NEW_IP"
    
    fi
    
    exit 0

    This script queries DNS record for your domain and compares the IP address with the one obtained from https://checkip.amazonaws.com. If they match, it does nothing. If they don’t match, the script updates the Cloudflare’s DNS record. The script uses curl for the API calls and awk to parse the responses.

    Now create a cron job (with crontab -e command) to run this script every <X> minutes (adjust to taste). Change <user> to the username where you stored the script.

    */<X> * * * * /home/<user>/.ddns/ddns.sh >/dev/null 2>&1

    Obtaining the DNS token

    To get the Zone ID and the API key, go to your Cloudflare portal, select the domain you need. In the overview scroll down and select “Click to copy” underneath the Zone ID field and paste it into the variables file for the ZONE_ID variable. Then click “Get your API token” to create your API token.

    Now, click on the blue “Create Token” button.

    Next, click the “Use template” button next to the “Edit zone DNS”.

    There edit the Token Name. It’ll be important to keep track of all your tokens if you create many. In the “Zone Resources” select “Specific zone” and select the domain you’re creating the token for. And then click “Continue to summary button”.

    Not much to do here but to click the “Create Token” button.

    This is where you finally get your token. Make sure you copy it into the variables file for the KEY variable as you won’t be able to view it again and will need to recreate it.

    This is it. This is the method I use to track my IP addresses for the devices with the dynamic public IPs. I then query the DNS record to get the IP and update the allow lists on my firewall (as described in Firewalls and DDNS). Of course, there’s a delay in the DNS record propagation, plus the frequency at which you run the script, so it may not be fully suitable for the mission critical applications. But if you’re running a mission critical application, you’re probably not relying on dynamic IPs and use a static IP anyways…

  • The Magic of Meraki

    Recently, I was fortunate to spend a few months working for Cisco, doing tech support for their Meraki products. As an avid self-hoster, I was a bit apprehensive about the idea of a cloud managed networking platform. But boy was I wrong.

    The whole experience was a lot of fun. I helped foreign ministries with their Meraki onboarding. I had a pleasure of assisting an animation studio that played a great role in my life growing up with their WiFi troubles. I troubleshot client VPN issues for my favorite watch brand, discovering an unexpected behavior, and worked with the product team to improve the customer experience.

    I don’t think anyone has doubts that Meraki’s hardware is solid. Meraki is a part of Cisco, they know how to build networking equipment. In many ways, they are Networking. But other vendors make solid networking equipment as well. And I would argue that Meraki’s secret sauce is not the hardware, it’s the Meraki Dashboard.

    Meraki Dashboard is a management plane for the Meraki equipment (duh). It allows for full management control of the equipment. In fact, there’s very little you can do on the Meraki devices locally: just set up an IP address, and get a SDB (support data bundle). And I experienced its power.

    As an “enterprise. at home” enthusiast, I wanted to lay my hands on a Meraki device, their MX security appliance, in particular. After hours of deliberations on which MX to get, I got a Z4 teleworker gateway. The Z-series appliances are also known as “baby MX”, so I think it was fitting.

    On the way home, sitting in the airport with nothing better to do, I decided to configure my yet-to-be-received teleworker gateway. I created a Meraki Dashboard organization, a network, and went ahead configuring it. I created VLANs, decided on the IP subnetting scheme, created SSIDs and firewall rules.

    A few days later, when I got my Z4 (every time I say Z4, I think of BMW, it’s really a shame that they’re discontinuing their Z4), I claimed it to my organization and assigned it to the network I created while at the airport. And that’s it. All I needed to do was to plug a network cable into the port labeled “Internet” and to provide power. The Z4 pulled the configuration from the dashboard and was up and running. I guess I still needed to plug it in, so not “true” zero-touch provisioning. Maybe 0.2-touch provisioning? Rounding down it would be zero-touch provisioning, good enough for me.

    This experience inspired me to explore the Meraki platform for the homelab application further. I’ll do a few posts on how it fits into my work flow, what works, what doesn’t, and the overall experience with the platform. We’ll probably start with the device onboarding, then will go into IPSec VPN and, its magical sibling, Meraki Auto-VPN.

    OK, this post’s been long enough, I better wrap it up. Till next time…

  • Firewalls and DDNS

    In the SSH post, I showed a command

    sudo ufw allow from AAA.BBB.CCC.DDD to any port ssh proto tcp

    to open up access to your Debian based SSH server through ufw. In this tutorial we’ll be using 2 machines: our server which we’re accessing using SSH, and our workstation. It’s pretty simple in the home LAN environment, replace AAA.BBB.CCC.DDD with your workstation’s IP address, like 192.168.1.201, or whatever it is, and you’re done. But what if the server is remote and your workstation’s home ISP only provides you a dynamic IP address? That’s where DDNS comes in.

    DDNS, or Dynamic DNS, or DynDNS, is a method of updating the DNS record that points to your network. When your ISP changes your external IP address (when your router reboots, for example), a DDNS client can send the new address to a DDNS provides who updates the DNS record. Some home routers have this functionality built in and they monitor your IP address and update DDNS as needed. But they usually work with a limited number of providers. There are also software clients that you can run on your system, ddclient, for example. And you can run it on any (almost) machine in your network as your router has only one external IP address regardless of the number of internal devices it’s routing to. Let’s not think about the situations with dual WAN, or if you restrict a machine to route all traffic through a VPN. If you’re doing that you probably can figure out how to set DDNS up.

    But don’t want to install additional packages and would rather achieve the same result with a help of Bash scripts. We’ll be setting up two scripts. One on the workstation that will keep the DNS record up to date, and another one on the server that will look at the DNS record and update its firewall to allow SSH access only from that IP address. We need the second script as ufw does not work with the domain names but only with the IP addresses. So we’ll need a way to resolve the domain name into the IP and tell ufw to allow that IP.

    If you’re going with the implementation on your router, use the services programmed in it. But I will be of no help to you as I don’t know what router you have. Instead, I’ll focus on how to update your DNS record from a Linux machine.

    So how do we set DDNS up? First, we need a FQDN, or a Fully Qualified Domain Name, like itsfreeatleast.blog (or itsfreeatleast.com). Since we want it for free, I suggest we use DuckDNS. It’s a free DDNS service that allows you to have up to 5 domains (such as example.duckdns.org). DuckDNS even gives you specific instruction on how to create a script that automatically updates your DNS record here.

    Important fields to note on the DuckDNS page (after you login) are the domain and the token, example and abcdef0-1234-5678-9101-112131fedcba, respectively.

    Now let’s create a script to update the DuckDNS record. We’ll save the script somewhere your user has access to and name it something like duckdnsupdate.sh. You don’t need to be root to run this script, so the home directory is fine. Adjust the values for the domain <example> and the token <abcdef0-1234-5678-9101-112131fedcba> to the ones shown for you. Use your favorite text editor, I prefer nano.

    nano $HOME/duckdnsupdate.sh

    Copy and paste the following script:

     #!/bin/bash
    
    DOMAIN = <example>
    TOKEN = <abcdef0-1234-5678-9101-112131fedcba>
    
    OLD_IP=$(/usr/bin/host $DOMAIN.duckdns.org | /usr/bin/awk '/has address/ { print $4 }')
    if [[ "$OLD_IP" =~ ^(([1-9]?[0-9]|1[0-9][0-9]|2([0-4][0-9]|5[0-5]))\.){3}([1-9]?[0-9]|1[0-9][0-9]|2([0-4][0-9]|5[0-5]))$ ]]; then
        :
    else
      /usr/bin/echo "Failed getting Old IP. Using a dummy address."
      OLD_IP="10.111.111.111"
    fi
    /usr/bin/echo "Old IP is $OLD_IP"
    
    NEW_IP=$(/usr/bin/curl -s --connect-timeout 20 https://checkip.amazonaws.com)
    if [[ "$NEW_IP" =~ ^(([1-9]?[0-9]|1[0-9][0-9]|2([0-4][0-9]|5[0-5]))\.){3}([1-9]?[0-9]|1[0-9][0-9]|2([0-4][0-9]|5[0-5]))$ ]]; then
      /usr/bin/echo "New IP is $NEW_IP"
    else
      /usr/bin/echo "Failed getting new IP"
      exit 1
    fi
    
    if [ "$NEW_IP" = "$OLD_IP" ]; then
      /usr/bin/echo "IP is already correct"
    else
    /usr/bin/echo url="https://www.duckdns.org/update?domains=$DOMAIN&token=$TOKEN&ip=" | /usr/bin/curl -s --connect-timeout 20 -k -o /tmp/.duckdns/duck.log -K - && /usr/bin/echo "New IP updated!" || /usr/bin/echo "New IP Update Failed!"
    fi
    
    exit 0

    Exit with CTRL + X, Y to save changes, and Enter.

    Now, let’s break it down.

    First, we query a DNS server for the IP address of our domain and assign it to a variable OLD_IP with

    OLD_IP=$(/usr/bin/host $DOMAIN.duckdns.org | /usr/bin/awk '/has address/ { print $4 }')

    Then we check whether the value looks like a legit IP address:

    if [[ "$OLD_IP" =~ ^(([1-9]?[0-9]|1[0-9][0-9]|2([0-4][0-9]|5[0-5]))\.){3}([1-9]?[0-9]|1[0-9][0-9]|2([0-4][0-9]|5[0-5]))$ ]];

    If it is fine, we continue, if not (it timed out, or connection is down, for example), we use a “dummy” IP address. It can be anything.

    Next, we get our external IP address. For that we query https://checkip.amazonaws.com. We use the curl command and we also want to include a connection timeout (20 seconds) and the same validity check as with the OLD_IP.

    NEW_IP=$(/usr/bin/curl -s --connect-timeout 20 https://checkip.amazonaws.com)
    if [[ "$NEW_IP" =~ ^(([1-9]?[0-9]|1[0-9][0-9]|2([0-4][0-9]|5[0-5]))\.){3}([1-9]?[0-9]|1[0-9][0-9]|2([0-4][0-9]|5[0-5]))$ ]];

    If everything goes fine, we assign the IP to a variable NEW_IP, if not, we exit the script.

    Then, we compare the OLD_IP (the one the DNS knows) with the NEW_IP (our current external IP). If they match we print "IP is already correct" on the screen and exit. If they don’t match, we send our new IP to DuckDNS and update the record for our domain like so:

    /usr/bin/echo url="https://www.duckdns.org/update?domains=$DOMAIN&token=$TOKEN&ip=" | /usr/bin/curl -s --connect-timeout 20 -k -o /tmp/.duckdns/duck.log -K - && /usr/bin/echo "New IP updated!" || /usr/bin/echo "New IP Update Failed!"

    This command includes a 20 second timeout and prints out whether the IP update was successful or not. It also places the status update in the file /tmp/.duckdns/duck.log.

    Next we’ll make sure only our user owns the file (the chown command) and we’ll the file executable (the chmod command). I also like to tighten down the permissions just a bit.

    chown $(id -un):$(id -gn) $HOME/duckdnsupdate.sh
    chmod 500 $HOME/duckdnsupdate.sh

    Now let’s automate it. We’ll periodically check what the DNS server thinks our IP is, compare it with ours, and update, as needed. I think 15 minute interval is fine, but you can use whatever value you find acceptable. Updating it too frequently places additional burden on the DuckDNS servers, which is not a nice thing to do. Updating it too infrequently will make you wait longer if your external IP updates. We’ll use the good old crontab for this. You can use the systemd timers, but crontab is fine for this.

    Use the following command to edit your crontab. It may ask you which text editor to use, again, I prefer nano.

    crontab -e

    Add the following lines to the bottom (make sure you change <username> to your user, in other words, make sure you use the absolute path to the script duckdnsupdate.sh.

    @reboot /usr/bin/sleep 5 && /home/<username>/duckdnsupdate.sh > /dev/null 2>&1
    */15 * * * * /home/<username>/duckdnsupdate.sh > /dev/null 2>&1

    Save and exit with CTRL + X, Y, and Enter.

    Now, let me explain. The first line tells crontab that after reboot it should wait for 5 seconds and execute the script. The wait is just to ensure the workstation got the internet connection. You may adjust this value. The second line calls the script every 15 minutes. The syntax of crontab is weird, so you can use a generator. > /dev/null 2>&1 is to suppress any text output as we don’t need it.

    I prefer to use absolute paths in crontab. It is better for security and here’s why.

    If you run the command

    echo $PATH

    it will show you everything that is contained in the global environmental variable PATH, like so:

    /usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games

    When you call a command, let’s say ls, Linux looks through the paths in the environmental variable PATH for the binary called ls. It does so folder by folder in the order from let to right. On my system, and probably on yours too, ls is located in /usr/bin/ls. You can verify it by running which ls command. So, if a malicious actor is able to access the folder /usr/local/bin and place a malicious file named ls in there, next time you call the ls command, the system will execute /usr/local/bin/ls instead of /usr/bin/ls. The malicious actor can even add folders to the beginning of the PATH variable and store their malicious binaries there. This can lead to privilege escalation and a whole bunch of heartburn. So if you don’t use the absolute variables and the malicious actor performs this type of attack, next time your crontab runs, it will execute the malicious binary. Scary stuff…

    OK, we’re done with our workstation. Now it updates the DNS record for example.duckdns.org to our IP address keeps periodic checks for changes.

    Now, let head to our server and allow SSH from only our IP address. We’ll do the steps similar to the workstation setup. We’ll create a script file and add it to crontab. We will need to run the script as root, though, because only root can update the firewall parameters. OK, let’s go.

    sudo nano /root/ufwallow.sh

    Adjust the hostname and the SSH port (default 22) to match your setup. The log file location can stay as is.

    #!/bin/bash
    HOSTNAME=<example>.duckdns.org
    LOGFILE=/root/ufwallowlog
    SSH_Port=<22>
    
    Current_IP=$(/usr/bin/host $HOSTNAME | /usr/bin/awk '/has address/ { print $4 }')
    if [[ "$Current_IP" =~ ^(([1-9]?[0-9]|1[0-9][0-9]|2([0-4][0-9]|5[0-5]))\.){3}([1-9]?[0-9]|1[0-9][0-9]|2([0-4][0-9]|5[0-5]))$ ]]; then
    #    :
      /usr/bin/echo "IP for $HOSTNAME is $Current_IP"
    else
      /usr/bin/echo "Failed getting IP"
      exit 1
    fi
    
    if [ ! -f $LOGFILE ]; then
        /usr/sbin/ufw allow from $Current_IP to any port $SSH_Port proto tcp
        /usr/bin/echo $Current_IP > $LOGFILE
    else
    
        Old_IP=$(/usr/bin/cat $LOGFILE)
        if [ "$Current_IP" = "$Old_IP" ] ; then
            /usr/bin/echo "IP address has not changed"
        else
            /usr/sbin/ufw allow from $Current_IP to any port $SSH_Port proto tcp
            /usr/sbin/ufw delete allow from $Old_IP to any port $SSH_Port proto tcp
            /usr/bin/echo $Current_IP > $LOGFILE
            /usr/bin/echo "ufw has been updated"
        fi
    fi

    Exit with CTRL + X, Y to save changes, and Enter.

    Here’s the breakdown:

    Current_IP=$(/usr/bin/host $HOSTNAME | /usr/bin/awk '/has address/ { print $4 }')
    if [[ "$Current_IP" =~ ^(([1-9]?[0-9]|1[0-9][0-9]|2([0-4][0-9]|5[0-5]))\.){3}([1-9]?[0-9]|1[0-9][0-9]|2([0-4][0-9]|5[0-5]))$ ]];

    This section gets the current IP address of our workstation using DNS and it checks if the number makes sense. If it does, it assigns it to Current_IP, if not, it exits.

    Then it checks if the file /root/ufwallowlog exits and is not empty. Actually, it check the inverse of that, the if statement is true if the file does not exits. This file keeps track of what IP ufw allows SSH access from. If the file does not exit, we just add the rule to ufw.

    if [ ! -f $LOGFILE ]; then
        /usr/sbin/ufw allow from $Current_IP to any port $SSH_Port proto tcp
        /usr/bin/echo $Current_IP > $LOGFILE

    If it does exit, we read the value from it and compare it to the Current_IP address.

        Old_IP=$(/usr/bin/cat $LOGFILE)
        if [ "$Current_IP" = "$Old_IP" ] ;

    If they’re the same, the script just tells us so "IP address has not changed". If it has changed, the script allows access from Current_IP and deletes the rule allowing Old_IP. It then overwrites the log file with the Current_IP for use next time.

    /usr/sbin/ufw allow from $Current_IP to any port $SSH_Port proto tcp
    /usr/sbin/ufw delete allow from $Old_IP to any port $SSH_Port proto tcp
    /usr/bin/echo $Current_IP > $LOGFILE
    /usr/bin/echo "ufw has been updated"

    After creating the file we’ll make sure it’s owned by root and only root can read and execute it:

    sudo chown root:root /root/ufwallow.sh
    sudo chmod 500 /root/ufwallow.sh

    It’s probably a good idea to make sure only root can write to the log file:

    sudo chown root:root /root/ufwallowlog
    sudo chmod 600 /root/ufwallowlog

    Now, let’s add it to root‘s crontab the instructions to run this script periodically:

    sudo nano crontab -e

    add to the end:

    @reboot /usr/bin/sleep 5 && /root/ufwallow.sh > /dev/null 2>&1
    */15 * * * * /root/ufwallow.sh > /dev/null 2>&1

    Again, you can adjust the parameters as you see fit.

    And this is it…

    OK. It’s a lot of words. But now we have a fully qualified domain name which is being updated from our workstation with our external IP address. Our remote SSH server periodically queries a DNS server to resolve the IP of our workstation and makes sure the IP can access the SSH server. Updates are made as necessary. What I noticed, though, that DuckDNS sometimes is slow to resolve the domains into IP addresses. It happens on some corporate networks. Maybe next time we’ll look at the alternatives. But hey, it’s free, at least…