Table of Contents

About

The UPC Connect Box is yet another piece of shit router™ that carries a very high degree of mysticism. Once you are past the patronising and politically correct figurines on the gargantuan and lame splash pages, you will find that the whole interface is an abominable web of HTML intercalated with JavaScript such that all the elements on the page are generated dynamically and there is unfortunately not sensible way to retrieve the external IP address. Similar to other plastic consumer equipment, the network device has no built-in SNMP, no console and no other way to obtain some programatic output.

Selenium and PhantomJS can be used instead of retrieve the external IP address by automatically navigating the menus headless and clicking through JavaScript events through the enormous clusterfuck that UPC customers have to put up with.

It does not even mention a brand or maker in the entire interface - it is also nameless and known as "Connect box"! The manual has been scooped off the Internet from various UPC.tld websites that, for some reason, probably have to host the PDF files.

Requirements

Setup

Download the script in the code section, configure the script by editing the parameters in the configuration section at the top and then place it in, say /etc/cron/cron.hourly. The script is created such that it will not return any output in case the CloudFlare name already resolves to the "Connect box" external address. However, the script will announce when the IP has been updated or in case there are any errors in attempting to do so.

Retrieving the IP Address

The script to retrieve the IP Address in Python simply follows the interface by:

  1. connecting to the "Connect Box" web-interface IP.
  2. filling-in the password (it's in plaintext, over HTTP and no, there is no username with this thing. . .)
  3. clicking the button to log-in.
  4. locating the "Admin" drop-down menu and clicking it.
  5. locating the "Info" drop-down sub-menu item and clicking it.
  6. waiting for the IP information to be filled out by the "Connect Box" JavaScript.
  7. retrieving the WanIPv4 element's inner-text which gives us the external IP.

all the while ensuring that the logout link is clicked, otherwise the "Connect Box" will be stuck until it is rebooted because "someone else is changing settings".

#!/usr/bin/python
# -*- coding: utf-8 -*-
 
# imports and packages
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.support import expected_conditions as EC
from contextlib import contextmanager
import unittest, time, re, time, os, sys, random, signal
 
# the tool takes as parameter:
# - the IP address of the UPC Connect Box router
# - the username
# - the password
if len(sys.argv) != 3:
    print "Syntax: " + sys.argv[0] + " " + "<IP>" + " " + "<password>"
    sys.exit(1)
upc = sys.argv[1]
pwd = sys.argv[2]
 
try:
    # initialize PhantomJS
    wd = webdriver.PhantomJS(service_log_path='/var/ghostdriver.log')
    # initialize wait
    wait = WebDriverWait(wd, 60)
    wd.get('http://' + upc)
    wd.find_element_by_id('loginPassword').click()
    wd.find_element_by_id('loginPassword').clear()
    wd.find_element_by_id('loginPassword').send_keys(pwd)
    wait.until(EC.element_to_be_clickable((By.NAME,'id_common_login')))
    wd.find_element_by_name('id_common_login').click()
    # "Admin" menu item.
    wait.until(EC.element_to_be_clickable((By.XPATH,'//*[text() = "Admin"]')))
    wd.find_element_by_xpath('//*[text() = "Admin"]').click()
    # "Admin->Info" menu item.     
    wait.until(EC.element_to_be_clickable((By.NAME, 'common_page/Info')))
    wd.find_element_by_name('common_page/Info').click();
    # "WanIPv4" element contents.
    wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="WanIPv4" and text() !=""]')))
    print wd.find_element_by_id('WanIPv4').text
except:
    # Tough luck!
    #print "Unexpected error:", sys.exc_info()[0]
    pass
finally:
    # Logout if possible.
    try:
        wd.find_element_by_id("c_mu30").click()
    except:
        # Tough luck!
        pass
    wd.close()
    wd.service.process.send_signal(signal.SIGTERM)
    wd.quit()

Code

upc-connectbox-cloudflare.sh
#!/bin/sh
###########################################################################
##  Copyright (C) Wizardry and Steamworks 2017 - License: GNU GPLv3      ##
##  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  ##
##  rights of fair usage, the disclaimer and warranty conditions.        ##
###########################################################################
 
###########################################################################
##                            CONFIGURATION                              ##
###########################################################################
 
# Connect Box router IP or hostname
CONNECT_BOX_LOCAL_ADDRESS='192.168.0.1'
# Connect Box router password
CONNECT_BOX_PASSWORD='myplaintextsecret'
# Set this to the cloudflare zone name.
DOMAIN='randy.com'
# Set this to the A record name.
RECORD_NAME='forward.randy.com'
# Set this to your account e-mail address.
EMAIL='me@domain.tld'
# Set this to your CloudFlare API key.
API_KEY='c928dae55b8edcadbc98c079c921db77f53a8'
 
###########################################################################
##                               INTERNALS                               ##
###########################################################################
 
LOCK_FILE='/var/lock/upc-connectbox-cloudflare'
 
# Acquire a lock.
if mkdir $LOCK_FILE 2>/dev/null; then
    trap '{ rm -rf $LOCK_FILE; }' KILL QUIT TERM EXIT INT HUP
else
    exit 0
fi
 
# Retrieve the external address.
CONNECT_BOX_EXTERNAL_ADDRRESS=`cat << EOF | /usr/bin/env python - $CONNECT_BOX_LOCAL_ADDRESS $CONNECT_BOX_PASSWORD
# -*- coding: utf-8 -*-
 
# imports and packages
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.support import expected_conditions as EC
from contextlib import contextmanager
import unittest, time, re, time, os, sys, random, signal
 
# the tool takes as parameter:
# - the IP address of the ZTE router
# - the username
# - the password
if len(sys.argv) != 3:
    print "Syntax: " + sys.argv[0] + " " + "<IP>" + " " + "<password>"
    sys.exit(1)
upc = sys.argv[1]
pwd = sys.argv[2]
 
try:
    # initialize PhantomJS
    wd = webdriver.PhantomJS(service_log_path='/var/ghostdriver.log')
    # initialize wait
    wait = WebDriverWait(wd, 60)
    wd.get('http://' + upc)
    wd.find_element_by_id('loginPassword').click()
    wd.find_element_by_id('loginPassword').clear()
    wd.find_element_by_id('loginPassword').send_keys(pwd)
    wait.until(EC.element_to_be_clickable((By.NAME,'id_common_login')))
    wd.find_element_by_name('id_common_login').click()
    # "Admin" menu item.
    wait.until(EC.element_to_be_clickable((By.XPATH,'//*[text() = "Admin"]')))
    wd.find_element_by_xpath('//*[text() = "Admin"]').click()
    # "Admin->Info" menu item.     
    wait.until(EC.element_to_be_clickable((By.NAME, 'common_page/Info')))
    wd.find_element_by_name('common_page/Info').click();
    # "WanIPv4" element contents.
    wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="WanIPv4" and text() !=""]')))
    print wd.find_element_by_xpath('//*[@id="WanIPv4"]').text
except:
    # Tough luck!
    #print "Unexpected error:", sys.exc_info()[0]
    pass
finally:
    # Logout if possible.
    try:
        wd.find_element_by_id("c_mu30").click()
    except:
        # Tough luck!
        pass
    wd.close()
    wd.service.process.send_signal(signal.SIGTERM)
    wd.quit()
 
EOF
`
 
### DEBUG
#echo $CONNECT_BOX_EXTERNAL_ADDRRESS
#exit 0
 
if [ -z $CONNECT_BOX_EXTERNAL_ADDRRESS ]; then
    echo "Unable to retrieve Connect Box external address."
    exit 1
fi
 
FLARE=$(curl -s https://www.cloudflare.com/api_json.html \
    -d 'a=rec_load_all' \
    -d "tkn=$API_KEY" \
    -d "email=$EMAIL" \
    -d "z=$DOMAIN")
 
if [ -z $FLARE ]; then
    echo "Unable to retrieve CloudFlare zone."
    echo "Please check the API key."
    exit 1
fi
 
ZONE_ADDRESS=`echo $FLARE | sed -E "s/.*,\"name\":\"$RECORD_NAME\",[^}]*?,\"type\":\"A\",[^}]*?,\"content\":\"([0-9\.]+?)\",.*/\1/"`
if [ -z $ZONE_ADDRESS ]; then
    echo "Unable to retrieve the CloudFlare zone IP address."
    exit 1
fi
 
# if the Connect Box external address matches the zone address terminate
if [ $CONNECT_BOX_EXTERNAL_ADDRRESS = $ZONE_ADDRESS ]; then
    exit 0
fi
 
RECORD_ID=`echo $FLARE | sed -E "s/.*\"rec_id\":\"([0-9]+?)\",[^}]*?,\"name\":\"$RECORD_NAME\",[^}]*?,\"type\":\"A\",.*/\1/"`
if [ -z $RECORD_ID ]; then
    echo "Unable to retrieve CloudFlare record ID for the zone."
    exit 1
fi
 
FLARE=$(curl -s https://www.cloudflare.com/api_json.html \
    -d 'a=rec_edit' \
    -d "tkn=$API_KEY" \
    -d "id=$RECORD_ID" \
    -d "email=$EMAIL" \
    -d "z=$DOMAIN" \
    -d 'type=A' \
    -d "name=$RECORD_NAME" \
    -d "content=$CONNECT_BOX_EXTERNAL_ADDRRESS" \
    -d 'service_mode=0' \
    -d 'ttl=1')
 
RESULT=`echo $FLARE | sed -E "s/.*,\"result\":\"([^\"]*)\",.*/\1/"`
if [ $RESULT = "success" ]; then
    echo "$RECORD_NAME now points to $CONNECT_BOX_EXTERNAL_ADDRRESS"
    exit 0
fi
 
exit 1

Code Cloudflare API v4

Compared to the former, the following script will update all A records created with Cloudflare to set the IP to the IP of the Connect Box.

#!/bin/sh
###########################################################################
##  Copyright (C) Wizardry and Steamworks 2021 - License: GNU GPLv3      ##
##  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  ##
##  rights of fair usage, the disclaimer and warranty conditions.        ##
###########################################################################
 
###########################################################################
##                            CONFIGURATION                              ##
###########################################################################
 
# CONNECT_BOX router IP or hostname
CONNECT_BOX_LOCAL_ADDRESS='192.168.1.1'
# CONNECT_BOX router password
CONNECT_BOX_PASSWORD='mypassword'
# Set this to the cloudflare zone name.
ZONE='myzone.tld'
# Set this to your account e-mail address.
CLOUDFLARE_AUTH_EMAIL='my@mail.tld'
# Set this to your CloudFlare API key.
CLOUDFLARE_AUTH_KEY='123e610f47ee66d08bcac633c4b445828d4e1'
 
###########################################################################
##                               INTERNALS                               ##
###########################################################################
 
LOCK_FILE='/var/lock/upc-connectbox-cloudflare'
 
# Acquire a lock.
if mkdir $LOCK_FILE 2>/dev/null; then
    trap '{ rm -rf $LOCK_FILE; }' KILL QUIT TERM EXIT INT HUP
else
    exit 0
fi
 
# Retrieve the external address.
CONNECT_BOX_EXTERNAL_ADDRRESS=`cat << EOF | /usr/bin/env python - $CONNECT_BOX_LOCAL_ADDRESS $CONNECT_BOX_PASSWORD
# -*- coding: utf-8 -*-
 
# imports and packages
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.support import expected_conditions as EC
from contextlib import contextmanager
import unittest, time, re, time, os, sys, random, signal
 
# the tool takes as parameter:
# - the IP address of the ZTE router
# - the username
# - the password
if len(sys.argv) != 3:
    print "Syntax: " + sys.argv[0] + " " + "<IP>" + " " + "<password>"
    sys.exit(1)
upc = sys.argv[1]
pwd = sys.argv[2]
 
try:
    # initialize PhantomJS
    wd = webdriver.PhantomJS(service_log_path='/var/ghostdriver.log')
    # initialize wait
    wait = WebDriverWait(wd, 60)
    wd.get('http://' + upc)
    wd.find_element_by_id('loginPassword').click()
    wd.find_element_by_id('loginPassword').clear()
    wd.find_element_by_id('loginPassword').send_keys(pwd)
    wait.until(EC.element_to_be_clickable((By.NAME,'id_common_login')))
    wd.find_element_by_name('id_common_login').click()
    # "Admin" menu item.
    wait.until(EC.element_to_be_clickable((By.XPATH,'//*[text() = "Admin"]')))
    wd.find_element_by_xpath('//*[text() = "Admin"]').click()
    # "Admin->Info" menu item.     
    wait.until(EC.element_to_be_clickable((By.NAME, 'common_page/Info')))
    wd.find_element_by_name('common_page/Info').click();
    # "WanIPv4" element contents.
    wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="WanIPv4" and text() !=""]')))
    print wd.find_element_by_xpath('//*[@id="WanIPv4"]').text
except:
    # Tough luck!
    #print "Unexpected error:", sys.exc_info()[0]
    pass
finally:
    # Logout if possible.
    try:
        wd.find_element_by_id("c_mu30").click()
    except:
        # Tough luck!
        pass
    wd.close()
    wd.service.process.send_signal(signal.SIGTERM)
    wd.quit()
 
EOF
`
 
### DEBUG
#echo $CONNECT_BOX_EXTERNAL_ADDRRESS
#exit 0
 
# get the zone id for the requested zone
ZONE_ID=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$ZONE&status=active" \
  -H "X-Auth-Email: $CLOUDFLARE_AUTH_EMAIL" \
  -H "X-Auth-Key: $CLOUDFLARE_AUTH_KEY" \
  -H "Content-Type: application/json" | jq -r '{"result"}[] | .[0] | .id')
 
A_RECORDS=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
     -H "Content-Type:application/json" \
     -H "X-Auth-Key: $CLOUDFLARE_AUTH_KEY" \
     -H "X-Auth-Email: $CLOUDFLARE_AUTH_EMAIL" | jq '.result[] | select(.type == "A") | .name')
 
for DNS_RECORD in $A_RECORDS; do
    DNS_RECORD=`echo $DNS_RECORD | tr -d \"`
    # get the dns record id
    DNS_RECORD_ID=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=A&name=$DNS_RECORD" \
    -H "X-Auth-Email: $CLOUDFLARE_AUTH_EMAIL" \
    -H "X-Auth-Key: $CLOUDFLARE_AUTH_KEY" \
    -H "Content-Type: application/json" | jq -r '{"result"}[] | .[0] | .id')
 
    # update the record
    curl --output /dev/null -s -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$DNS_RECORD_ID" \
    -H "X-Auth-Email: $CLOUDFLARE_AUTH_EMAIL" \
    -H "X-Auth-Key: $CLOUDFLARE_AUTH_KEY" \
    -H "Content-Type: application/json" \
    --data "{\"type\":\"A\",\"name\":\"$DNS_RECORD\",\"content\":\"$CONNECT_BOX_EXTERNAL_ADDRRESS\",\"ttl\":1,\"proxied\":false}"
done