Please enable JavaScript to view the comments powered by Disqus.

Teddy Hartanto

thoughts and experiments

When the SL and SR buttons of my Nintendo Switch Joy-Con broke, I looked up how much it would cost to get them repaired. It was way too expensive. The spare part is cheap, and the repair is straightforward.

I decided to order the part and do the repair myself. After successfully repairing my issue, I decided to turn it into a side business.


Here are the lessons I learned.

  1. Starting is difficult. I’m only doing repairs, but imagine starting up a more complex business. I had stripped screws, broken tools, damaged parts, missing parts, and delayed work, as well as uncomfortable conversations with customers. However, they’re all part of starting up. Fuck ups are normal. Fuck ups are signs of growth
  2. Fake it till you make it. I had impostor syndrome when I started. But, how do you become good at something without ever feeling like a fucking noob? You have to just do it. Make mistakes, learn from them, avoid them, plan better, do better.
  3. Cheapest repair service wins. Affordable repairs encourage people to consider making repairs. Why would people bother with repairing if the repair cost comes at around 50-60% of buying a new one? They'd have to bear the hidden cost of conversing with the repairman, meeting with him, waiting for the repair, etc.
  4. People don't mind going down to my home. Some were driving.
  5. Do the right thing. I realized that I can't afford to botch my job and focus purely on the money. Sometimes, people bring their joycons for repair so that their children can play. I don't want to disappoint those kids.
  6. All parts are not equal. For instance, the quality of analog joysticks varies between suppliers.
  7. Mind the hidden costs. I can profit about $11 per 10 minutes of easy repair jobs. That's a whopping $66/hr. However, that calculation is only valid if I perform repair work and the problem has been clearly identified. There were hidden costs: chatting with customers, meeting them, troubleshooting their joycons, testing them. Troubleshooting is especially time-consuming when dealing with a more complex case.

There is a parallel between a skilled repairman and a doctor. A good repairman doesn't need to open up the joycon and use measuring tools like a multimeter to determine where the faults are. He can narrow down or sometimes accurately diagnose the faults by asking questions and testing for functions (symptomatic troubleshooting).

For example, a customer said that their joycon's battery is spoilt. I asked them what they mean by that. They told me that they plugged their joycon into the Switch console and the console didn't detect it. I wouldn't be too quick to conclude that this is an issue with the battery. It could be an issue with the charging rail connector, rather than a dead battery.

Likewise, a good doctor rules out diseases by testing for functions (such as movement and reflexes) instead of jumping into asking patients to undergo diagnostic procedures like CT scans and MRIs.


If I clear my inventory, I'm supposed to make about 5x the capital I invested into spare parts. But I couldn't clear my inventory. I had to stop my repair service due to life circumstances. I did break even, though. I managed to cover the tools I bought: a microscope, an iFixit Pro Tech Toolkit, etc.


I bought a pair of Timberland boat shoes on Amazon for SGD80. They're at least SGD200 in physical stores. (I don't exactly remember because I posted this in 2025 when I should've done it in 2022)


A derivation work of https://theo-andreou.org/.

Today, I had to migrate my blog from hosting it in AWS to DigitalOcean. Before I began hosting my blog, I already had a t2.micro EC2 instance running as part of my experimentation with Linux. I thought it was convenient to host my blog in that same server. Well, it was... until the bill came. Apparently, the instance is only free for the first 12 months. After that, AWS charges me for a whopping $18/mo. Freak. I decided to switch to DO to reduce the cost to $6/mo.

It took me a couple hours to re-setup everything. So, I'm writing this in case I ever need to do the same. Perhaps this would save me some time.

Warning: I have NOT tested everything below. I merely picked commands from my history and cross-reference them to the link I gave above

# Set up OS

# Set up WriteFreely
# Install nginx & certbot
$ sudo apt install -y nginx certbot python3-certbot-nginx

# Install mysql
$ wget https://dev.mysql.com/get/mysql-apt-config_0.8.22-1_all.deb
$ sudo dpkg -i mysql-apt-config*
$ sudo apt install mysql-community-server mysql-server

# Create a writefreely system user (good practice)
$ sudo useradd -r -m -d /srv/writefreely writefreely

# Install WriteFreely
$ cd /srv/writefreely
$ wget https://github.com/writefreely/writefreely/releases/download/v0.13.1/writefreely_0.13.1_linux_amd64.tar.gz
$ sudo -u writefreely tar xfvz writefreely_0.13.1_linux_amd64.tar.gz
$ sudo -u writefreely mv writefreely teddyh.dev

# Prepare DB
$ sudo mysql
mysql> CREATE DATABASE writefreely CHARACTER SET latin1 COLLATE latin1_swedish_ci;
mysql> CREATE USER 'writefreely'@'localhost' IDENTIFIED BY 'password';
mysql> GRANT ALL PRIVILEGES ON writefreely.* TO 'writefreely'@'localhost'; 

# Copy over config

# Copy over dump file
ubuntu@old-node~$ sudo mysqldump writefreely > dump.sql
ubuntu@old-node~$ scp dump.sql teddy@new-node
teddy@new-node~$ mysql -u writefreely -p writefreely < dump.sql

# Copy / create a WriteFreely service (same as theo andreo's)
$ systemctl daemon-reload 
$ systemctl enable --now writefreely.service
$ curl localhost:8080  # test

# Copy / create nginx config (same as theo andreo's)
# Replace example.org -> teddyh.dev
$ cd /etc/nginx/sites-enabled/
$ sudo  ln -s ../sites-available/example.org
$ sudo nginx -t && sudo systemctl reload nginx

# Set up certbot
$ sudo certbot --nginx

This is not a how-to-play guide. This is a how-to-win guide.

Five cards are dealt to both my opponent and me.

Game starts.

Now, let's define some notations for convenience:

A, B, C, D, E denotes any random character in Coup
i_have(nX) denotes the event that I was dealt n number of X cards, where X ∈ {A, B, C, D, E}. If undefined, n == 1
u_have(nX) denotes the event that my opponent was dealt n number of X cards, where X ∈ {A, B, C, D, E}. If undefined, n == 1
P(Y) denotes the probability of some event
C(Y) denotes the combination of some event
nCr denotes the number of combinations of n pick r

The winning formula is simple:

  1. Guess what cards your opponent has
  2. Guess what cards your opponent pick
  3. Prepare bluffs
  4. Play

1. Guess what cards your opponent has

There are two signals:

  1. The cards you are dealt with tells you what cards your opponent might have
  2. How long your opponent spends on card-picking tells you how many choices or even the choices they have
Given i_have(0A), 
    P(u_have(>=1A))
        = 1 - C(u_have(0A)) / C(any)
        = 1 - 7C5 / 10C5
        = 0.92
    P(u_have(2A))
        = C(u_have(2A)) / C(any)
        = 3C2 * 7C3 / 10C5
        = 0.42
    P(u_have(3A))
        = C(u_have(3A)) / C(any)
        = 7C2 / 10C5
        = 0.08
Given i_have(1A),
    P(u_have(>=1A))
        = 1 - C(u_have(0A)) / C(any)
        = 1 - 8C5 / 10C5
        = 0.78
    P(u_have(1A))
        = C(u_have(1A)) / C(any)
        = 8C4 * 2C1 / 10C5
        = 0.56
    P(u_have(2A))
        = C(u_have(2A)) / C(any)
        = 8C3 / 10C5
        = 0.22
Given i_have(2A),
    P(u_have(1A))
        = C(u_have(1A)) / C(any)
        = 9C4 / 10C5
        = 0.5
        which makes sense because your opponent can pick any 1 of the two piles containing A

Possible cards dealt to you and what they mean:

- i_have(ABCDE)
   - assume the opponent has all different
- i_have(2A0BCDE)
    - assume the opponent has all different
- i_have(2A2BC0D0E)
    - assume the opponent has CDE and either A or B.
- i_have(3ABC0D0E)
    - assume the opponent has DE and either B or C

Implications:

  • I can lie about the cards I don’t have. There’s only an 8% chance my opponent has all 3 of the identical cards. It’s still risky though. There’s a 42% chance my opponent has at least 2 of the same. They might just shoot at 42%
  • If I have 2 of the same character, calling my opponent’s bluff of that character is risky. Getting it right is 50-50

I can further guess what cards were dealt to my opponent by the time spent picking their cards. This is highly subjective, but we need to consider whose turn it is to make the first move and what cards the opponent might have. Usually, the more time they spend, the more choices they have. It could also mean that they have bad cards.

Always be the last to pick cards unless I want to throw off my opponent.

2. Guess what cards your opponent pick

This is a subjective assessment, but it becomes easier and easier to guess as you play more and learn more about your opponent's playing style. If they were dealt >=2 of Duke, do they usually not pick Duke and bluff their way?

Depending on who's the first to move:

  • They are first to move: they would want the assassin
  • I am first to move: they would want contessa

3. Prepare to bluff

  • If I don’t have a captain & ambassador, and the opponent wants to steal coins, what do I say?
  • Assassin & Contessa has an asymmetric payoff
  • Ambassador is the most useless

4. Play

  • Getting the opponent to reveal their card first is always better. I can validate my guess with better accuracy. They lose one ability. When it’s my turn to reveal, I can plan for the best course of action
  • Ignore bluffs if you can still win without calling their bluffs

Meta-strategy

Remember how your opponent plays:

  • When they keep losing because of an invalid call of bluffs, do they keep trying, or do they stop trying?
  • Do they tend to lie when they have 2 of the same character?
  • Do they tend to be honest?
  • What combination of cards do they usually use? When they reveal 1, it’s easier to guess the other.

Their strategy changes, too. When they keep getting called on bluffs, they might begin playing honestly.


What is a computer?

At its most basic level, a computer is an electronic device capable of performing computations. That implies a set of hardware that enables computation: a processor and a random-access memory.

A PC has more than that. It has:

  • a storage device
  • a network interface

A lot of things are computers. Sometimes, even components we would typically not think of as a computer (well, at least not me) — a network interface card. A NIC has its own processor and RAM. It's a special-purpose computer designed to connect to a network of computers. It sends & inspects packets. If it receives a packet that is not meant for its host, it discards the packet. It doesn't interrupt its host. Imagine if it can't compute, and the host has to attend to every single packet. That would slow down the host.

What else is a computer?

  • A Trusted Platform Module (TPM) is a computer
  • A Baseboard Management Controller (BMC) is a computer
  • A Power Distribution Unit (PDU) is a computer
  • A router is a computer
  • A switch is a computer

Computers with network interfaces are especially useful, as we can communicate with them remotely and instruct them to perform or display specific actions.


I run WriteFreely as a systemd service. To run it in standalone mode (binding directly to port 80 & 443), I had to add a certain variable in its unit file (configuration file):

[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE

In Problems When Setting Up WriteFreely Part 2: Firewall, I explained how I couldn't access my blog over https because of the firewall I've set up ages ago and forgotten.

I showed the outputs of two curls:

╭─teddy@teddy-ubuntu ~ 
╰─$ curl -vvv http://teddyh.dev
*   Trying 54.151.219.0:80...
* TCP_NODELAY set
* connect to 54.151.219.0 port 80 failed: Connection refused
* Failed to connect to teddyh.dev port 80: Connection refused
* Closing connection 0
curl: (7) Failed to connect to teddyh.dev port 80: Connection refused
╭─teddy@teddy-ubuntu ~ 
╰─$ curl -vvv https://teddyh.dev
*   Trying 54.151.219.0:443...
* TCP_NODELAY set
* connect to 54.151.219.0 port 443 failed: Connection timed out
* Failed to connect to teddyh.dev port 443: Connection timed out
* Closing connection 0
curl: (28) Failed to connect to teddyh.dev port 443: Connection timed out

I failed to notice this at the time, but there is a subtle difference between the two outputs: Connection refused vs Connection timed out.

What do they mean?

It is easier to understand if we take a peek at the TCP layer when the curl requests are being sent:

Connection refused

─teddy@teddy-ubuntu ~ 
╰─$ sudo tcpdump -nn "host teddyh.dev and port 80"  
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on enp7s0, link-type EN10MB (Ethernet), capture size 262144 bytes
10:33:25.540898 IP 192.168.10.117.43884 > 54.151.219.0.80: Flags [S], seq 2847222361, win 64240, options [mss 1460,sackOK,TS val 564702358 ecr 0,nop,wscale 7], length 0
10:33:25.543802 IP 54.151.219.0.80 > 192.168.10.117.43884: Flags [R.], seq 0, ack 2847222362, win 0, length 0

Connection timed out

(Induced by firewall DROP rule)

╭─teddy@teddy-ubuntu ~ 
╰─$ sudo tcpdump -nn "host teddyh.dev and port 443"                                                                                                                                                          130 ↵
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on enp7s0, link-type EN10MB (Ethernet), capture size 262144 bytes
10:34:34.541405 IP 192.168.10.117.44816 > 54.151.219.0.443: Flags [S], seq 1786361284, win 64240, options [mss 1460,sackOK,TS val 564771358 ecr 0,nop,wscale 7], length 0
10:34:35.569866 IP 192.168.10.117.44816 > 54.151.219.0.443: Flags [S], seq 1786361284, win 64240, options [mss 1460,sackOK,TS val 564772387 ecr 0,nop,wscale 7], length 0
10:34:37.589852 IP 192.168.10.117.44816 > 54.151.219.0.443: Flags [S], seq 1786361284, win 64240, options [mss 1460,sackOK,TS val 564774407 ecr 0,nop,wscale 7], length 0
^C

*Note: Client IP address is 192.168.10.117, Server IP address is 54.151.219.0

Interpretation

Connection refused

The client tried to open a connection (SYN), but the server closed the connection (RST). Put simply, you (the client) knocked on your friend Bob's front door, and his mom (the server) told you, “Bob's not home!”

How could this happen?

  1. Bob's mom was lying. Bob was grounded, so his mom didn't want you to meet Bob. This is analogous to a firewall REJECT rule
  2. Bob's mom was telling the truth. Bob indeed wasn't at home. This is analogous to no service/process listening to the destined port on the host.

Connection timed out

Connection timed out means the client kept trying to open a connection to the destined port on the host, but there was no reply — so the client gave up trying. The client attempted to open a connection (SYN) repeatedly, but received no reply from the server. Put simply, you (the client) kept knocking on your friend Bob's front door and there was no answer.

How could this happen?

  1. Bob's at home but didn't hear you knocking. The sound of your knocks didn't reach him. This is analogous (well, not really) to a network congestion
  2. Bob's whole family kept quiet and didn't answer the door. They hear you, but they choose not to answer you. This is analogous to a firewall DROP rule
  3. Bob's whole family is not at home. This is analogous to a powered-off host. (I've confirmed this by trying to open a TCP connection to a powered-off host.) Contrast this with the Connection refused scenario in which we received a response from the server — indicating that the host is up.

Commenting is a feature lacking in WriteFreely. As a blog author, I want to hear your opinions on my posts. Do you agree with what I wrote? Do you know something I don't? Was I wrong in certain things?

Disqus came to mind because I've seen it on many sites. The installation is supposedly hassle-free:

  1. Sign up
  2. Create a site on Disqus
  3. Embed the JavaScript code
  4. Voila!

I added the JS snippet into the “Post Signature.” Any content in that field should be rendered in each post.

Well... apparently, any contents but <script></script> 🤦‍♂️

Sigh. That is quite limiting. It was an intentional decision in the name of security. The rationale was reasonable. But I disagree with the solution. The argument is invalid when WriteFreely is in single-user mode.

I see a couple of ways to achieve my goal:

  1. Request for Custom Javascript to be enabled on single-user mode
  2. Modify the source, build, and deploy a. to disable sanitization on “Post Signature” b. to include the custom Javascript
  3. Find loopholes to inject custom Javascript

Option 2 is slow. Option 1 is viable, but it would require some time and effort. Meanwhile, option 3 has been discovered. That came in handy 🙂. Ignore for a second that the solution is hacky. I argue that it is pretty ingenious. Plus, it's the quickest way to achieve my goal. Kudos to infuerno for discovering that loophole.

That solution works well for me, particularly because I only display post titles on my blog's index page. By default, Disqus comment threads are identified by the page URL in which they are loaded. Since I force readers to visit the post page, each post gets its own unique comment thread.

It's dirty, but hey, it works! 🤷‍♂️ I'll leave it until I can get a better solution.

Now, go on and leave some comments!

I allowed Guest Comments if you would like to remain anonymous. However, pre-moderation is enforced by default for guest comments. In other words, guest comments require my approval to go public.


In Problems When Setting Up WriteFreely Part 2: Firewall, I posed an unanswered question:

Why did netstat not show me processes listening to port 80 & 443, yet I was able to make an HTTP request to port 80?

In that post, I showed a netstat output as follows:

[email protected]:~$ netstat -t4lpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:33060         0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -       

I wanted to confirm that my WriteFreely instance was listening to port 80 & 443. But, the above output didn't give me the answer I sought. I didn't see anything listening on port 80, yet I was able to connect to that port successfully!

I only focused on IPv4 at the time because I thought I was connecting to the IPv4 interface of my server. After all, my server wasn't assigned a public IPv6 address. teddyh.dev only has an A record containing the IPv4 address of my server.

Later, I did more research on this seemingly odd phenomenon. I stumbled upon this SO post. Apparently, AF_INET6 sockets can receive connections from IPv4 addresses!

Had I used sudo netstat -tlpn instead, I would have seen the following output:

[email protected]:~$ sudo netstat -tlpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      459/systemd-resolve 
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      707/sshd: /usr/sbin 
tcp        0      0 127.0.0.1:33060         0.0.0.0:*               LISTEN      644/mysqld          
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      644/mysqld          
tcp6       0      0 :::22                   :::*                    LISTEN      707/sshd: /usr/sbin 
tcp6       0      0 :::443                  :::*                    LISTEN      140750/writefreely  
tcp6       0      0 :::80                   :::*                    LISTEN      140750/writefreely

This output provided more insights than netstat -t4lpn. The last 2 lines showed that WriteFreely was listening to :::80 & :::443. That would have given me some clue about this probable relationship between IPv6 sockets and IPv4 client addresses.

To further demonstrate this compatibility, I used telnet to open a TCP connection to my server.

My home PC:

╭─teddy@teddy-ubuntu ~ 
╰─$ telnet teddyh.dev 443                                                                                                                                                                                    130 ↵
Trying 54.151.219.0...
Connected to teddyh.dev.
Escape character is '^]'.

My blog server:

[email protected]:~$ sudo netstat -t6apn
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp6       0      0 :::22                   :::*                    LISTEN      707/sshd: /usr/sbin 
tcp6       0      0 :::443                  :::*                    LISTEN      140750/writefreely  
tcp6       0      0 :::80                   :::*                    LISTEN      140750/writefreely            
tcp6       0      0 172.31.29.106:443       A.B.C.D:36134     ESTABLISHED 140750/writefreely  

I have redacted my home IP address above to A.B.C.D. Notice that both the local & foreign addresses are IPv4. 172.31.29.106 is the private IPv4 address of my server, NAT'ed from the public IPv4 address 54.151.219.0 shown in the output of the telnet command.

The above experiment proves that AF_INET6 sockets are compatible with IPv4 addresses. That said, that compatibility can be overridden by setting the IPV6_V6ONLY option, as pointed out in the linked StackOverflow post.


This is a two-part series. Read part 1 here.

In my previous post, I ran into an HSTS issue. I bought a domain name that appears to be HSTS preloaded. That means whenever I use a modern browser to visit my blog, it will be strictly loaded over HTTPS. My plan to simply load my blog over HTTP was foiled. To work around this, I ran WriteFreely in standalone mode, which supports SSL out of the box. With that, I solved the problem. I can now visit https://teddyh.dev!


Or so I thought.

At least until I found out that I still couldn’t load my blog. I was still seeing the same ERR_CONNECTION_TIMED_OUT in my browser. How come?

╭─teddy@teddy-ubuntu ~ 
╰─$ curl -vvv https://teddyh.dev
*   Trying 54.151.219.0:443...
* TCP_NODELAY set
* connect to 54.151.219.0 port 443 failed: Connection timed out
* Failed to connect to teddyh.dev port 443: Connection timed out
* Closing connection 0
curl: (28) Failed to connect to teddyh.dev port 443: Connection timed out
╭─teddy@teddy-ubuntu ~ 
╰─$ curl -vvv http://teddyh.dev
*   Trying 54.151.219.0:80...
* TCP_NODELAY set
* Connected to teddyh.dev (54.151.219.0) port 80 (#0)
> GET / HTTP/1.1
> Host: teddyh.dev
> User-Agent: curl/7.68.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 302 Found
< Content-Type: text/html; charset=utf-8
< Location: https://teddyh.dev/
< Date: Tue, 14 Jun 2022 08:05:09 GMT
< Content-Length: 42
< 
<a href="https://teddyh.dev/">Found</a>.

* Connection #0 to host teddyh.dev left intact

The fact that I got a proper response over http should indicate that my standalone WriteFreely setup was working as intended. Indeed, the documentation said that WriteFreely should redirect http connections to https. However, I couldn't see any log of the http connections I made. Either WriteFreely doesn't log the http redirects, or something else was handling the redirects.

I was fairly certain that WriteFreely was handling the redirects. However, I had to verify. I tried checking for open ports using netstat:

[email protected]:~$ netstat -t4lpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:33060         0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -                   

I saw that nothing was listening on ports 80 and 443. I scratched my head. How come? Earlier, I was able to get a response from port 80! (That shall be the topic of my next post)

Anyways, I tried another method to verify — I stopped WriteFreely and tried curling again:

╭─teddy@teddy-ubuntu ~ 
╰─$ curl -vvv http://teddyh.dev
*   Trying 54.151.219.0:80...
* TCP_NODELAY set
* connect to 54.151.219.0 port 80 failed: Connection refused
* Failed to connect to teddyh.dev port 80: Connection refused
* Closing connection 0
curl: (7) Failed to connect to teddyh.dev port 80: Connection refused
╭─teddy@teddy-ubuntu ~ 
╰─$ curl -vvv https://teddyh.dev
*   Trying 54.151.219.0:443...
* TCP_NODELAY set
* connect to 54.151.219.0 port 443 failed: Connection timed out
* Failed to connect to teddyh.dev port 443: Connection timed out
* Closing connection 0
curl: (28) Failed to connect to teddyh.dev port 443: Connection timed out

Seeing how my http connection was refused, I concluded that, indeed WriteFreely was the one redirecting http->https. Still, it puzzles me why I couldn't connect to the https port.

I hadn't notice this at the time, but there was a subtle difference in the errors reported above: Connection refused vs Connection timed out. That should've been a clue.

It took me some time and some visits to Google & StackOverflow before it dawned on me: maybe a firewall was blocking https requests!

I checked my iptables and saw the word ufw scattered around. Aha! I remembered that I had once played with ufw on this server — as part of Linux Upskill Challenge. I've completely forgotten it because that was a long time ago.

Sure enough, I saw https traffic was treated with the ufw default incoming DENY policy (since only ssh and http traffic is allowed):

[email protected]:~$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       Anywhere                  
80/tcp                     ALLOW       Anywhere                  
22/tcp (v6)                ALLOW       Anywhere (v6)             
80/tcp (v6)                ALLOW       Anywhere (v6)             

Once I configured ufw to allow https traffic, I was able to visit my blog on my browser! :)

This concludes the two-part series on the problems I encountered when setting up my WriteFreely instance. Writing these posts have been truly rewarding! I learned a new command (tcpdump), discovered new tricks with old commands, and gained a better understanding of Linux and networking. It made me better at troubleshooting, too.

Running my blog and writing down my learnings have paid huge dividends! :)

Next, I'll cover some things I learned in the process of troubleshooting:

Stay tuned!


PS:

It didn't cross my mind at the time, but I could've ssh into my server and curl my local port 443:

[email protected]:~$ curl -I https://teddyh.dev --resolve 'teddyh.dev:443:127.0.0.1'
HTTP/2 200 
content-type: text/html; charset=utf-8
date: Tue, 14 Jun 2022 08:52:53 GMT

Had I done so, I would have been assured that my WriteFreely setup was working correctly. It would have also pointed me to look at my firewall rules sooner.

On a side note, troubleshooting a confluence of issues can be challenging. It's easy to assume that A is caused by B and B only. But, it could've been caused by B, C, and D. I guess multi-factor problems like these reflect life more accurately, wherein issues are often caused by multiple colluding variables.