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.
aptitude install selenium
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.
The script to retrieve the IP Address in Python simply follows the interface by:
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()
#!/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
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