Network

Getting the Hasivo F1100W-4SX-4XGT switch to work

TL:DR The missing (documentation) for this otherwise fine switch

I was first exposed to Cisco network equipment in 1995, and for a long time I used their gear for my home network. Then around 2012 I switched to Juniper switches for their more sane management interface and more reasonable prices. Neither have WiFI equipment that’s really appropriate for a home setting, however, with onerous licensing terms or crackpot schizophrenic hardware like my old Cisco 877W that was one half ADSL router and one half WiFi AP (coexisting uneasily in the same physical box with separate management interfaces).

By the time I got fed up with single consumer APs, their lack of coverage and tendency to burn out within a year due to inadequate power supplies, I bit the bullet, went with the Ubiquiti Networks UniFi solution that I use to the day. At one point I considered switching to TP-Link Omada, but procrastination paid off, and I dodged a bullet.

Unfortunately, Ubiquiti doesn’t have switches with both SFP+ interfaces (for fiber optic connections) and 10G-BaseT (for copper like on my Mac Studio), other than the expensive, bulky and non-fanless Pro HD 24. While you can easily get 10G-BaseT copper SFP+ modules, the power draw of a 10G-BaseT port is actually more than the nominal power capacity of a SFP+ port and in my experience they are unreliable. For a while, I used the ZyXEL XGS1250-12, which has 3 2.5G/10G copper and one SFP+, but I would prefer a switch with more SFP+ ports.

After digging through reviews, and ServeTheHome, I found out about Hasivo switches, an inexpensive Chinese brand offering great value for money and interesting port configurations. Their F1100W-4SX-4XGT offers 4x SFP+ and 4x 2.5G/10G copper ports for £152.39 plus VAT, so I ordered one. When I received it, I plugged it into my home office Ubiquiti USW Pro Max 16 PoE with a Ubiquiti 10G DAC cable, plugged my Mac, and everything just worked as it should.

The Power and RET LEDs were flashing red and green, however. Furthermore, this is supposed to be a L3-capable switch with a Web UI, but no DHCP request or IP appeared in my UniFi console or IP address (spoiler: it’s 192.168.0.1, and DHCP is not enabled by default). The switch did not include any documentation, there is nothing available on the Hasivo site, not even in Chinese (they have documentation links, but they point to a completely different product, and even then are largely useless).

Here’s how I got it to work, using information gleaned from various Internet forums:

  1. First, get a Cisco-style RJ45 serial console cable like the Cable Matters one, hook it to the Console port on the Hasivo.
  2. Start a terminal session, in my case on Linux:
chown uucp:uucp /dev/ttyUSB0
cu -l /dev/ttyUSB0 -s 38400
  1. The login is admin and the password is admin

  2. The terminal console UI is a knockoff of the Cisco IOS CLI:

    1. enable to enter administrator mode
    2. conf t to enter configuration mode
    3. interface vlan1 to configure the admin interface
    4. ip address 10.254.254.115/8 (or whatever you want it to be)
    5. exit to go back to the interface level
    6. exit to go back to the global config level
    7. show interface brief to verify the config was applied correctly
    8. copy running-config startup-config to make the changes persistent
  3. You can now point your web browser to http://10.254.254.115/

Some braver people than myself are attempting to get OpenWRT running on the switch, but they doesn’t seem to have succeeded yet.

Fiber for your home network

TL:DR Fiber as the backbone of your home network is easier than you’d think

My apartment, like many, is elongated. The living room is on one end, the bedrooms (one of which is my home office), on the other side. This makes it hard to cover both sides with a single WiFi access point, or to have uniform Internet access speed on the wired network. I have a semi-pro Ubiquiti UniFi network of WiFi access points and switches, which makes it realtively easy, but only if you have good backhaul connectivity between the APs.

For the longest time I used G.hn powerline networking bridges made by Devolo. Unfortunately, powerline is at least as unreliable as wireless networking and this made for frequent brown-outs requiring unplugging the Devolo Magic 2 boxes to power-cycle them. I know Devolo doesn’t make the actual PLC controllers and their firmware, probably made by Broadcom from the MAC addresses, but surely they could implement something as simple as a watchdog timer to reboot the PLC if no heartbeats are seen in a while?

At any rate, at some point I flipped the bozo switch on the Devolos and decided it was long past time to install proper Ethernet across the two halves of the apartment, which is easier said than done in a rental. To make things more complex, my Internet connection that used to be absymal Vodafone/BT OpenReach VDSL ending in my office was upgraded to a fiber ISP but this terminated by the door in half-way no-man’s land.

This was around the time I was experimenting with 10G Ethernet in the core of my home network, using Ubiquiti’s relatively inexpensive (for the time) USW-Aggregation switches with 8 SFP+ 10G ports. Speaking of which, while you can buy 10GBase-T SFP+ modules allowing you to use copper 10G Ethernet like the one on my Mac Studio, their power draw exceeds the specs of the SFP+ standard and they are unreliable, stick with fiber or use a switch with actual 10G ports (in my case a ZyXEL XGS1250-12, although it has an unfortunate tendency to overheat).

Contrary to what you may think, multimode fiber is much thinner (thus more discreet) and far more flexible than copper Ethernet cable (fiber above in the picture, copper below).

Fiber and copper cable compared

I conceived the idea of running a 30m pre-terminated fiber cable, made by a French company, as it turns out, along the crown molding in the ceiling, held in place with transparent plastic 3M Command hooks originally meant for holding Christmas lights, and easily removed without damage to the paintwork (this is a rental, remember).

Fiber cable on the ceiling

I had to run it along a snaking route in red to stay along the crown molding, but even with my tyro DIY skills it only took a couple of hours to set up and is barely visible unless you know to look for it. While I don’t actually have any 10G devices in my living room yet, I do have a WiFi7 access point and it won’t be bottlenecked by the Ethernet network.

Floor plan

I still have a Devolo link between my office and the AP in my bedroom, but that’s a much shorter distance and much less unreliable.

PSA: iCloud Private Relay can make Safari on your iPad unusable

After upgrading my iPad to iPadOS 15.5, Safari became unusable. It would take forever to load the Reddit login page, and many others like Dilbert.com. Opening the same in Firefox Focus had no issues.

Going into Settings / Safari / Privacy & Security / Hide IP Address and disabling it fixed this for me. Alternatively you can disable it only for specific networks (Settings / Wi-Fi / ⓘ / Limit IP Address Tracing / Off).

It seems Apple turned on iCloud Private Relay on by default for Safari in iPadOS 15.5 and presumably iOS 15.5 as well. Macs are probably next.

I can only speculate why turning it off fixes the breakage, but:

  • The feature routes your calls through Akamai then CloudFlare, and for whatever reason CloudFlare doesn’t seem to like my ISP, I often encounter their “prove you are human” challenges.
  • It may also be because Apple overrides your DNS settings for this feature to work, and if your network is locked down with something like Pi-Hole to prevent trackers, those DNS requests may not be getting through. I don’t want IoT devices or the like to bypass my DNS server, which uses Wireguard to my Cloud VPN server to ensure my ISP cannot snoop on my DNS requests (a setup I believe more secure and private than Apple’s), nor CloudFlare, nor the UK Police State. I haven’t blocked DNS-over-HTTPS servers yet as this guy does but it’s on my list. This might be interfering with iCloud Private Relay.
  • It may also be sabotage, as Rui Carmo points out, or as John Oliver memorably calls it, “Cable Company F∗∗∗ery”.

Setting HTTP headers for a static site on AWS CloudFront

TL:DR This is way, way more complicated than it needs to be

For a very long time, I ran this site off my cloud server in the US. When I moved to London, I started experiencing the painful impact of the ~100ms latency on the loading time for images and videos, and decided to move to a Content Delivery Network (CDN) with global reach. Unfortunately, most CDNs have steep minimum spend requirements that are excessive for a low-traffic site like this one. Amazon’s CloudFront is an exception, and my hosting costs are in the vicinity of $20 per month, which is why I settled for it despite my dislike for Amazon.

Serving a static site is not just about putting content somewhere to be served over HTTPS. You also need to set up HTTP headers:

  • Cache-Control headers to ensure static content isn’t constantly checked for changes.
  • Security Headers to enable HSTS and protect your users from abuse like Google FLoC, sites that iframe your content or XSS injection.

In my original nginx configuration, this is trivial if a bit verbose, just add:

expires: max;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
add_header Content-Security-Policy "default-src 'self' https://*.majid.info/ https://*.majid.org/; object-src 'none'; frame-ancestors 'none'; form-action 'self' https://*.majid.info/; base-uri 'self'";
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Xss-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy no-referrer-when-downgrade;
add_header Feature-Policy "accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; battery 'none'; camera 'none'; display-capture 'none'; document-domain 'none'; encrypted-media 'none'; execution-while-not-rendered 'none'; execution-while-out-of-viewport 'none'; fullscreen 'none'; geolocation 'none'; gyroscope 'none'; layout-animations 'none'; legacy-image-formats 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; navigation-override 'none'; oversized-images 'none'; payment 'none'; picture-in-picture 'none'; publickey-credentials-get 'none'; sync-xhr 'none'; usb 'none'; vr 'none'; wake-lock 'none'; screen-wake-lock 'none'; web-share 'none'; xr-spatial-tracking 'none'; notifications 'none'; push 'none'; speaker 'none'; vibrate 'none'; payment 'none'";
add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=(), conversion-measurement=(), focus-without-user-activation=(), hid=(), idle-detection=(), serial=(), sync-script=(), trust-token-redemption=(), vertical-scroll=(), notifications=(), push=(), speaker=(), vibrate=(), interest-cohort=()";

Doing this with CloudFront is much more complicated, however. You have to use a stripped-down and specialized version of their AWS Lambda “serverless” Function-as-a-Service framework, Lambda@Edge. This is very poorly documented, so this is my effort at rectifying that. When I first set this up, only Node.js and Python were available, but it seems Go, Java and Ruby were added since. I will use Python for this discussion. The APIs are quite different for each language so don’t assume switching languages is painless.

In the interests of conciseness, I am going to skip the parts about creating a S3 bucket and enabling it for CloudFront. There are many tutorials available online. I use rclone to deploy actual changes to S3, and make an AWS API call using awscli to trigger a cache invalidation, but software like Hugo has built-in support for AWS. Here is my deployment target in my Makefile:

deploy:
	git push
	git push github master
	-rm -rf awspublic
	env HUGO_PUBLISHDIR=awspublic hugo --noTimes
	-rm -f awsindex.db
	env HUGO_BASE_URL=https://blog.majid.info/ ./fts5index/fts5index -db awsindex.db -hugo
	rclone sync -P awspublic s3-blog:fazal-majid
	rsync -azvH awspublic/. bespin:hugo/public
	scp awsindex.db bespin:hugo/search.db
	ssh bespin svcadm restart fts5index
	aws cloudfront create-invalidation --distribution-id E************B --paths '/*'

First of all, even though Lambda@Edge runs everywhere CloudFront does, you cannot create functions everywhere, so you will need to go to the Lambda functions console then switch your region to US-West-1 in your AWS Console drop-down menu (even though my CloudFront and S3 are in eu-west-2 (London).

Click on the Create Function button.

Then choose Author from scratch, give a name (in my case, SecurityHeaders) and choose the Python 3.8 runtime.

In the development environment, click on lambda_function.py to edit the code of your function.

Click on Deploy (which is really more of a Save button), then press the orange Test button. Choose the Event Template cloudfront-modify-response-header. Save it, e.g. TestHeaders and click again on the Test button to verify the function executes without exceptions.

Here is the code I use:

def lambda_handler(event, context):
    cf = event["Records"][0]["cf"]
    response = cf["response"]
    headers = response["headers"]
    headers['strict-transport-security'] = [{
      "key": "Strict-Transport-Security",
      "value": "max-age=31536000; includeSubDomains; preload"
    }]
    headers['content-security-policy'] = [{
      "key": "Content-Security-Policy",
      "value": "default-src 'self' https://*.majid.info/ https://*.majid.org/; object-src 'none'; frame-ancestors 'none'; form-action 'self' https://*.majid.info/; base-uri 'self'"
    }]
    headers['x-frame-options'] = [{
      "key": "X-Frame-Options",
      "value": "SAMEORIGIN"
    }]
    headers['x-xss-protection'] = [{
      "key": "X-Xss-Protection",
      "value": "1; mode=block"
    }]
    headers['x-content-type-options'] = [{
      "key": "X-Content-Type-Options",
      "value": "nosniff"
    }]
    headers['referrer-policy'] = [{
      "key": "Referrer-Policy",
      "value": "no-referrer-when-downgrade"
    }]
    headers['feature-policy'] = [{
      "key": "Feature-Policy",
      "value": "accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; battery 'none'; camera 'none'; display-capture 'none'; document-domain 'none'; encrypted-media 'none'; execution-while-not-rendered 'none'; execution-while-out-of-viewport 'none'; fullscreen 'none'; geolocation 'none'; gyroscope 'none'; layout-animations 'none'; legacy-image-formats 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; navigation-override 'none'; oversized-images 'none'; payment 'none'; picture-in-picture 'none'; publickey-credentials-get 'none'; sync-xhr 'none'; usb 'none'; vr 'none'; wake-lock 'none'; screen-wake-lock 'none'; web-share 'none'; xr-spatial-tracking 'none'; notifications 'none'; push 'none'; speaker 'none'; vibrate 'none'; payment 'none'"
    }]
    headers['permissions-policy'] = [{
      "key": "Permissions-Policy",
      "value": "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=(), conversion-measurement=(), focus-without-user-activation=(), hid=(), idle-detection=(), serial=(), sync-script=(), trust-token-redemption=(), vertical-scroll=(), notifications=(), push=(), speaker=(), vibrate=(), interest-cohort=()"
    }]
    headers['x-fm-version'] = [{
      "key": "x-fm-version",
      "value": str(context.function_version)
    }]
    # caching
    if "request" in cf and "uri" in cf["request"]:
      url = cf["request"]["uri"]
      ext = url.split('.')[-1].lower()
      if url.endswith('/') or ext in ('html', 'gif', 'png', 'jpg', 'jpeg', 'ico', 'css', 'js', 'eot', 'woff', 'mp4', 'svg'):
        headers['expires'] = [{
          "key": "Expires",
          "value": "Thu, 31 Dec 2037 23:55:55 GMT"
        }]
        headers['cache-control'] = [{
          "key": "Cache-Control",
          "value": "max-age=315360000, immutable"
        }]
        
    return response

You will need to modify the hardcoded value for Content-Security-Policy, most likely you don't want your images and assets to be only served from https://*.majid.info/... Also, I cache all HTML forever in the browser, which may be more aggressive than you want if you update content more frequently than I do.

Before you can set up the hook, you will need to deploy your code to Lambda@Edge.

Now, this is very important. There are 4 different places a Lambda@Edge function can hook into.

If you deploy your function in the wrong place, most likely you will cause HTTP 500 errors until you can delete the bad trigger and redeploy, a process that takes an interminable 5–10 minutes to percolate through the CloudFront network (ask me how I know...). The hook (event trigger in Lambda@Edge parlance) is Viewer Response, unfortunately the deployment dialog defaults to Origin Request.

Click the disclaimer checkbox and press the Deploy button. It will take a few minutes to deploy to CloudFront, and then you can use curl or your browser’s developer console to verify the headers are sent. I include a header X-FM-Version to verify which version of the function was deployed.

fafnir ~>curl -sSL -D - -o /dev/null 'https://blog.majid.info/hsts-preload/'
HTTP/2 200 
content-type: text/html; charset=utf-8
content-length: 26260
x-amz-id-2: 3ndAsEvUgHDhUYxok9kDnaNCUeQ8QMCbVURoiyjQHc699mrHQvJpN7xwgUeAp7Ir/9Pd1sLwtOU=
x-amz-request-id: 0NDCZD7JEG55903A
date: Wed, 28 Apr 2021 17:55:17 GMT
x-amz-meta-mtime: 1618529322.342819304
last-modified: Thu, 15 Apr 2021 23:28:59 GMT
etag: "33eb01a86db2b3f800c7bee0b5c10c11"
server: AmazonS3
vary: Accept-Encoding
strict-transport-security: max-age=31536000; includeSubDomains; preload
content-security-policy: default-src 'self' https://*.majid.info/ https://*.majid.org/; object-src 'none'; frame-ancestors 'none'; form-action 'self' https://*.majid.info/; base-uri 'self'
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
x-content-type-options: nosniff
referrer-policy: no-referrer-when-downgrade
feature-policy: accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; battery 'none'; camera 'none'; display-capture 'none'; document-domain 'none'; encrypted-media 'none'; execution-while-not-rendered 'none'; execution-while-out-of-viewport 'none'; fullscreen 'none'; geolocation 'none'; gyroscope 'none'; layout-animations 'none'; legacy-image-formats 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; navigation-override 'none'; oversized-images 'none'; payment 'none'; picture-in-picture 'none'; publickey-credentials-get 'none'; sync-xhr 'none'; usb 'none'; vr 'none'; wake-lock 'none'; screen-wake-lock 'none'; web-share 'none'; xr-spatial-tracking 'none'; notifications 'none'; push 'none'; speaker 'none'; vibrate 'none'; payment 'none'
permissions-policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=(), conversion-measurement=(), focus-without-user-activation=(), hid=(), idle-detection=(), serial=(), sync-script=(), trust-token-redemption=(), vertical-scroll=(), notifications=(), push=(), speaker=(), vibrate=(), interest-cohort=()
x-fm-version: 22
expires: Thu, 31 Dec 2037 23:55:55 GMT
cache-control: max-age=315360000, immutable
x-cache: Hit from cloudfront
via: 1.1 f655cacd0d6f7c5dc935ea687af6f3c0.cloudfront.net (CloudFront)
x-amz-cf-pop: AMS54-C1
x-amz-cf-id: QkB3rN2hWiI8ah_EJ3x3bvjgbm_BrqhFG1GJ_f4po-Mc2rs_TjTF-g==

Needless to say, because of the convoluted nature of this process, and the high likelihood of making mistakes, you should test this on a non-production site before you try this on a live site.

If by error you associated the lambda function with the wrong event trigger, you can delete it by going through the different deployed versions of your function, finding the trigger and deleting it.

HSTS: surprisingly rare

HTTP Strict Transport Security (HSTS) is a critical security feature that allows a site to say “always use the secure HTTPS version, not the insecure unencrypted one”. There is a chicken-and-egg effect where the first time you access a website, you have no way to know if your site has HSTS turned on or not without accessing it, so browsers distribute a “HSTS Preload” list of domains for which it is turned on even if you have never accessed it before, as explained by Adam Langley of the Google Security Team. On Chromium based browsers you can check by accessing chrome://net-internals/#hsts. Yours truly is on the list, which means that almost every single device on the planet has a file with my name in it, to my never-ceasing amusement.

Someone asserted that most e-commerce and financial sites are registered with HSTS Preload. I have a pretty jaundiced view of banks’ security, the fact most of them consider sending 6-digit codes by SMS a valid form of two-factor authentication leads me to believe they mostly engage in security theater. So I used the official Google Chrome HSTS Preload portal to check.

I was shocked to find out that in fact not only is HSTS Preload very rare, but even HSTS itself is hardly present. None of the sites I checked use either:

Not even Amazon.com has it, despite being a company that operates a Certificate Authority.

The only explanation I can think of is that this is a deliberate product decision to make life easier on those annoying free WiFi with captive portals, at the expense of security.

Captive portals are those WiFi networks that don’t support IEEE 802.11u Hotspot 2.0, which means that instead of showing you a popup when you connect to WiFi asking you to agree to the terms of service, sign in to a paid WiFi service or whatever, it will instead hijack the first non-TLS HTTP request and show you the captive portal page instead (pro tip: use neverssl.com as the first page you access on those portals). If you were to only access https://amazon.com/, you would hang forever, whereas with http://amazon.com/ you would first get the captive portal page, then on reload the actual Amazon page.

The flip side is that anyone can set up a WiFi pineapple and SSLstrip in a Starbucks to impersonate their free WiFi, hijack your connection by issuing a deauthentication frame to force you to disconnect from Starbucks’ WiFi and connect instead to your fake Starbucks WiFi, and then the attacker can use the SSL stripping described by Adam Langley to steal your Amazon password, even if you have two-factor authentication enabled. Given how easy Amazon has made it to impersonate them, I am surprised this kind of scam is not more prevalent.