Replacing Orange Livebox by an EdgeRouter with FTTH

This article will mainly target people located in France, but who knows 🙂

Orange, a French ISP, provides FTTH with up to 2GB downling/600mbps uplink, but the usage (and rental…) of their set top box (aka Livebox) is mandatory.

As I’m using my own router (Ubiquiti EdgeRouter), the Livebox is useless to me, just taking some place and energy :-). The goal of this article is to explain how to remove it to use a router instead.

The Livebox 5 integrates an ONT. So in order to remove the LB5, I needed to find an external ONT. A friend gave me an “old” Huawei HG8010H, which is the one Orange used to provide with older Livebox versions.

The first thing to do is to gate the ONT accepted on the optical network, commonly know as O5 state (“operation state” – https://www.mdpi.com/2076-3417/8/10/1934/pdf). To reach this state, the ONT must authenticate itself on the carrier network. Orange used to rely on the SLID (subscriber line ID), but this is no longer used. It now only relies on the serial number of the ONT. It means that is necessary to change the Serial Number of the new ONT by the allowed one (the Livebox). Getting the “allowed” SN is easy: just go on the Livebox administration page:

Getting the Serial Number of the ONT from Livebox 5

Setting the same SN in the new ONT is possible if the ONT is not in the ISP locked mode. I will not cover how to unlock an Orange-provided HG8010 ONT, this can be found on the web with keyboard “restorehwmode.sh” !

Once the Serial Number is changed on the ONT to simulate the Livebox, the fiber link should come up in O5 mode::

Now that the fiber link is up – the router must be configured to access.

Orange is using multiple VLANs (internet, TV, SIP…). I will just be covering the Internet access as I’m not using TV nor phone.

For Internet, the VLAN 832 should be used, using DHCP and the “option 90”, which is used to authenticate the subscriber.

The tricky detail is the following: DHCP packets should be sent with a VLAN priority set to 6 ! Without doing that, device won’t be able to authenticate to the network.

Before configuring the router, several things will be needed, such as the Mac Address of the Livebox and the authentication data. This can be calculated using a JavaScript tool, using the username & password provided by Orange (famous “fti/xxxx”). I chose a different option, which consists in gathering those information from the Livebox.

Feel free to use this simple Python script to obtain the information and generate the configuration for your EdgeRouter:

import requests
import json
import re
import sys

def authenticate(ip,user,password):

    payload = {}
    payload["service"] = "sah.Device.Information"
    payload["method"] = "createContext"
    payload["parameters"] = {}
    payload["parameters"]["applicationName"] = "webui"
    payload["parameters"]["username"] = user
    payload["parameters"]["password"] = password

    headers = {}
    headers["Authorization"] = "X-Sah-Login"

    r = requests.post("http://{}/ws".format(ip), verify=False, data=json.dumps(payload), headers=headers)
    
    token = json.loads(r.text)["data"]["contextID"]
    cookie = r.headers['Set-Cookie'].split(';')[0]
    
    return token,cookie

def get_dhcp_conf(ip, token, cookie):
    payload = {}
    payload["service"] = "NeMo.Intf.data"
    payload["method"] = "getMIBs"
    payload["parameters"] = {}
    payload["parameters"]["mibs"] = "dhcp"

    headers = {}
    headers["Authorization"] = "X-Sah {}".format(token)
    headers["X-Context"] = "{}".format(token)
    headers["Content-Type"] = "application/x-sah-ws-4-call+json"
    headers["Cookie"] = "{};".format(cookie)

    r = requests.post("http://{}/ws".format(ip), verify=False, data=json.dumps(payload), headers=headers)

    data = json.loads(r.text)

    macaddr = data["status"]["dhcp"]["dhcp_data"]["SentOption"]["61"]["Value"]
    auth = data["status"]["dhcp"]["dhcp_data"]["SentOption"]["90"]["Value"]
    userclass = data["status"]["dhcp"]["dhcp_data"]["SentOption"]["77"]["Value"]
    vendorclass = data["status"]["dhcp"]["dhcp_data"]["SentOption"]["60"]["Value"]

    values = {}
    values["macaddr"] = macaddr
    values["auth"] = auth
    values["userclass"] = userclass
    values["vendorclass"] = vendorclass

    print("[+] MAC Address of the Livebox: {}".format(macaddr))
    print("[+] DHCP Option 90 Auth String: {}".format(auth))
    print("[+] Livebox UserClass: {}".format(userclass))
    print("[+] Livebox VendorClass: {}".format(vendorclass))
    
    return values


def gen_ubnt(eth, values):
    macaddr = ':'.join(re.findall('..',values["macaddr"]))
    auth = ':'.join(re.findall('..',values["auth"]))
    userclass = '"' + bytes.fromhex(values["userclass"]).decode('ascii').replace("+","\\053") + '"'
    vendorclass = '"' + bytes.fromhex(values["vendorclass"]).decode('ascii') + '"'

    print("[+] EdgeRouter Config:\n\n")

    print("set interfaces ethernet {} vif 832 address dhcp".format(eth))
    print("set interfaces ethernet {} vif 832 description \"Orange DHCP Internet\"".format(eth))
    print("set interfaces ethernet {} vif 832 dhcp-options client-option \"send vendor-class-identifier {};\"".format(eth, vendorclass))

    print("set interfaces ethernet {} vif 832 dhcp-options client-option \"request subnet-mask, routers, domain-name-servers, domain-name, broadcast-address, dhcp-lease-time, dhcp-renewal-time, dhcp-rebinding-time, rfc3118-auth;\"".format(eth))
    print("set interfaces ethernet {} vif 832 dhcp-options client-option \"send user-class {};\"".format(eth,userclass))
    print("set interfaces ethernet {} vif 832 dhcp-options client-option \"send rfc3118-auth {};\"".format(eth, auth))
    print("set interfaces ethernet {} vif 832 dhcp-options client-option \"send dhcp-client-identifier {};\"".format(eth, macaddr))
    print("set interfaces ethernet {} vif 832 dhcp-options default-route update".format(eth))
    print("set interfaces ethernet {} vif 832 dhcp-options default-route-distance 210".format(eth))
    print("set interfaces ethernet {} vif 832 dhcp-options global-option \"option rfc3118-auth code 90 = string;\"".format(eth))
    print("set interfaces ethernet {} vif 832 dhcp-options global-option name-server update".format(eth))
    print("set interfaces ethernet {} vif 832 egress-qos \"0:0 1:0 2:0 3:0 4:0 5:0 6:6 7:0\"".format(eth))


ip = sys.argv[1]
user = sys.argv[2]
password = sys.argv[3]

token,cookie = authenticate(ip,user,password)
print("[+] Authentication OK")
print("  Token: {}".format(token))
print("  Cookie: {}".format(cookie))

values = get_dhcp_conf(ip, token, cookie)
gen_ubnt("eth2", values)

Last problem: the VLAN Priority 6. The router uses ISC-DHCP as DHCP client, which relies on raw sockets. As such, it bypasses the “egress policy” you could define on the router.

The only solution is to patch & recompile the ISC-DHCP to hardcode the VLAN priority.

Patching and re-compiling ISC-DHCP

The easiest option is to use Docker! First of all, retrieve the package from Ubiquiti matching your router firmware version:

This archive includes all the package we need to recompile: edgeos-vyatta-dhcp.

Instanciate a Docker as follows:

docker run --rm -it debian:9.13 /bin/bash

Install necessary cross compilation toolchain:

apt-get update && apt-get install -y crossbuild-essential-mipsel vim
mkdir /data && cd /data

Copy the vyatta-dhcp3_4.1-ESV-R15-ubnt1+t5402460.dev.stretch.v2.0.9.24c30f9.tar.gz file to the /data in your docker:

docker cp vyatta-dhcp3_4.1-ESV-R15-ubnt1+t5402460.dev.stretch.v2.0.9.24c30f9.tar.gz <docker id>:/data/

Extract the archive

tar -xzvf vyatta-dhcp3_4.1-ESV-R15-ubnt1+t5402460.dev.stretch.v2.0.9.24c30f9.tar.gz

Now, patch the various files by getting inspired by the patch provided here:

https://github.com/shisva/USG_Orange/blob/master/patch_dhclient3/iscdhcp_priority.patch

Those files must be updated:

discover.c

icmp.c

lpf.c

raw.c

socket.c

For the version isc-dhclient-4.1-ESV-R15-P1, my patch file is:

--- a/common/discover.c
+++ b/common/discover.c
@@ -247,10 +247,6 @@ begin_iface_scan(struct iface_conf_list *ifaces) {
                log_error("Error creating socket to list interfaces; %m");
                return 0;
        }
-
-       /* Set Kernel Priority to 6 */
-       int val = 6;
-       setsockopt(ifaces->sock, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val));

        memset(&lifnum, 0, sizeof(lifnum));
 #ifdef ISC_PLATFORM_HAVELIFNUM
diff --git a/common/icmp.c b/common/icmp.c
index ca857e0..6f97f67 100644
--- a/common/icmp.c
+++ b/common/icmp.c
@@ -95,10 +95,6 @@ void icmp_startup (routep, handler)
                        return;
                }

-               /* Set Kernel Priority to 6 */
-               int val = 6;
-               setsockopt(icmp_state -> socket, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val));
-
 #if defined (HAVE_SETFD)
                if (fcntl (icmp_state -> socket, F_SETFD, 1) < 0)
                        log_error ("Can't set close-on-exec on icmp: %m");
diff --git a/common/lpf.c b/common/lpf.c
index 8111f38..fcf7db1 100644
--- a/common/lpf.c
+++ b/common/lpf.c
@@ -89,10 +89,6 @@ int if_register_lpf (info)
                log_fatal ("Open a socket for LPF: %m");
        }

-       /* Set Kernel Priority to 6 */
-       int val = 6;
-       setsockopt(sock, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val));
-
        memset (&ifr, 0, sizeof ifr);
        strncpy (ifr.ifr_name, (const char *)info -> ifp, sizeof ifr.ifr_name);
        ifr.ifr_name[IFNAMSIZ-1] = '\0';
@@ -499,10 +495,6 @@ get_hw_addr(const char *name, struct hardware *hw) {
                log_fatal("Can't create socket for \"%s\": %m", name);
        }

-       /* Set Kernel Priority to 6 */
-       int val = 6;
-       setsockopt(sock, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val));
-
        memset(&tmp, 0, sizeof(tmp));
        strcpy(tmp.ifr_name, name);
        if (ioctl(sock, SIOCGIFHWADDR, &tmp) < 0) {
diff --git a/common/raw.c b/common/raw.c
index b588f1b..a15f8ee 100644
--- a/common/raw.c
+++ b/common/raw.c
@@ -66,10 +66,6 @@ void if_register_send (info)
        if ((sock = socket (AF_INET, SOCK_RAW, IPPROTO_RAW)) < 0)
                log_fatal ("Can't create dhcp socket: %m");

-       /* Set Kernel Priority to 6 */
-       int val = 6;
-       setsockopt(sock, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val));
-
        /* Set the BROADCAST option so that we can broadcast DHCP responses. */
        flag = 1;
        if (setsockopt (sock, SOL_SOCKET, SO_BROADCAST,
diff --git a/common/socket.c b/common/socket.c
index 8f94a63..3fe3d09 100644
--- a/common/socket.c
+++ b/common/socket.c
@@ -189,10 +189,6 @@ if_register_socket(struct interface_info *info, int family,
                log_fatal("Can't create dhcp socket: %m");
        }

-       /* Set Kernel Priority to 6 */
-       int val = 6;
-       setsockopt(sock, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val));
-
        /* Set the REUSEADDR option so that we don't fail to start if
           we're being restarted. */
        flag = 1;
@@ -1178,10 +1174,6 @@ get_hw_addr(const char *name, struct hardware *hw) {
        }

  flag_check:
-       /* Set Kernel Priority to 6 */
-       int val = 6;
-       setsockopt(sock, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val));
-
        if (lifr.lifr_flags & (IFF_VIRTUAL|IFF_IPMP)) {
                hw->hlen = sizeof (hw->hbuf);
                srandom((long)gethrtime());

Finally, recompile DHCLIENT.

cd /data/edgeos-vyatta-dhcp
make -f debian/rules configure
CC=mipsel-linux-gnu-gcc CPP=mipsel-linux-gnu-cpp ./configure --host=mipsel-linux-gnu --cache-file=config.cache
make

Finally, copy the file recompiled to the router:

scp /data/edgeos-vyatta-dhcp/client/dhclient ubtn@router:/tmp/

Connect to the router and replace the existing version after backing-up.

sudo -i
cp /sbin/dhclient3 /sbin/dhclient3.orig
chown root:root /tmp/dhclient
chmod a+x /tmp/dhclient
mv /tmp/dhclient /sbin/dhclient3

After applying the configuration (generated by my script), it should be working 🙂

set interfaces ethernet eth2 vif 832 address dhcp
set interfaces ethernet eth2 vif 832 description "Orange DHCP Internet"
set interfaces ethernet eth2 vif 832 dhcp-options client-option "send vendor-class-identifier &quot;sagem&quot;;"
set interfaces ethernet eth2 vif 832 dhcp-options client-option "request subnet-mask, routers, domain-name-servers, domain-name, broadcast-address, dhcp-lease-time, dhcp-renewal-time, dhcp-rebinding-time, rfc3118-auth;"
set interfaces ethernet eth2 vif 832 dhcp-options client-option "send user-class &quot;\053FSVDSL_livebox.Internet.softathome.Livebox4&quot;;"
set interfaces ethernet eth2 vif 832 dhcp-options client-option "send rfc3118-auth 00:00:00:00:00:xx:xx:xx:xx....;"
set interfaces ethernet eth2 vif 832 dhcp-options client-option "send dhcp-client-identifier 01:xx:xx:xx:xx:xx:xx;"
set interfaces ethernet eth2 vif 832 dhcp-options default-route update
set interfaces ethernet eth2 vif 832 dhcp-options default-route-distance 210
set interfaces ethernet eth2 vif 832 dhcp-options global-option "option rfc3118-auth code 90 = string;"
set interfaces ethernet eth2 vif 832 dhcp-options global-option name-server update
set interfaces ethernet eth2 vif 832 egress-qos "0:0 1:0 2:0 3:0 4:0 5:0 6:6 7:0"

This article was inspired by multiple threads from the French forum “lafibre.info”.

Using Esendex API to send SMS with Zabbix (Webhook)

Zabbix is a very powerful and free monitoring & alerting solution.

If you want to extend the possibilities for alerting with custom actions, it is possible to use customized webhooks.

I am using ESENDEX, as a SMS provider, which overs a various set of API – for which Zabbix does not have a predefined webhook.

Here are the steps to use the REST api to send SMS alerts.

Administration > Media Types

Then select “create media type” in the upper right corner.

The API requires several parameters, and instead of hardcoding in the JavaScript snippet we will write the values, we can make them dynamic:

The URL used for the Esendex API is https://api.esendex.com/v1.0/messagedispatcher.

In the Script section, you must define a custom script to call the REST API. The language used is Javascript.

Here is the script details to be used:

try {
    var params = JSON.parse(value)

    Zabbix.Log(4, '[ Esendex webhook ] Started with params: ' + params) 
    
    var req = new CurlHttpRequest()

    req.AddHeader('Content-Type: application/json');
    req.AddHeader('Authorization: Basic ' + btoa(params.User + ':' + params.API_KEY));

    var fields = {}
    fields.accountreference = params.Account
    fields.messages = [ { to: params.To, body: params.Message } ]
    
    resp = req.Post(params.URL,
        JSON.stringify(fields)
    );
 
    if (req.Status() != 200) {
        throw 'Response code: ' + req.Status();
    }

    return 'OK';
}
catch (error) {
     throw 'Failed with error: ' + error;
}

You can then use this a new media type, to provide phone numbers to profiles, and receive alerts:

#Zabbix #esendex #REST #webhook #API