davideisinger.com

My personal website
Log | Files | Refs | README

xeiaso-net-ygnwtd.txt (23659B)


      1 [1] Xe
      2 [2] Blog
      3 [3] Contact
      4 [4] Resume
      5 [5] Talks
      6 [6] VODs
      7 [7] Signalboost
      8 
      9 gokrazy is really cool
     10 
     11 Published on 09/20/2023, 3515 words, 13 minutes to read
     12 
     13 An image of undefined - Nikon D3300, 35mm f/1.8 DX, a red formula one style car
     14 speeds along a racetrack with visible tire skidmarks. Photo by Xe Iaso I worked
     15 for Tailscale at the time of writing this article. I wrote this on my own time
     16 out of my own volition.
     17 
     18 When you deal with Linux, you end up hearing about "distributions" as different
     19 "flavors" of Linux combined with a bunch of other tools. This is mostly true,
     20 but it's slightly missing the forest for the trees.
     21 
     22 Consider this famous and often misunderstood quote by Richard Stallman:
     23 
     24     I'd just like to interject for a moment. What you're referring to as Linux
     25     is in fact, GNU/Linux, or as I've recently taken to calling it, GNU plus
     26     Linux.Linux is not an operating system unto itself, but rather another free
     27     component of a fully functioning GNU system made useful by the GNU
     28     corelibs, shell utilities and vital system components comprising a full OS
     29     as defined by POSIX.
     30 
     31 Many pages of ink have been spilled over analyzing this quote, and a lot of
     32 them fall short of really getting at the heart of the matter. What this
     33 actually means is something like this:
     34 
     35 By itself, Linux is useless. It does boot the system, it does interface with
     36 hardware, but without a bunch of other tools, it's not very useful. It's like a
     37 car without a steering wheel, or a boat without a rudder. It does something,
     38 but it's not very useful. The real value of things like the GNU project,
     39 systemd, openrc and other tools in that vein is that they make Linux useful.
     40 They make it into a complete system that you can use to do things. They are the
     41 proverbial steering wheel and rudder in the metaphor.
     42 
     43 Mara is hacker
     44 <[8]Mara> Fun fact, if you try to boot a Linux kernel without an init process,
     45 it'll just panic and crash!
     46 
     47 Most Linux systems on the face of the planet are built with GNU tools and
     48 utilities. In order to compile the Linux kernel, you need to use [9]GCC. In
     49 order to run ls to list files in the current directory, you need to use [10]GNU
     50 coreutils. Every dynamically linked program uses [11]glibc for performing basic
     51 system interactions like writing to files or opening network sockets.
     52 Everything is built on top of the GNU toolset. This is why Stallman is so
     53 adamant about calling it GNU/Linux. It's not that he's trying to take credit
     54 for Linux, it's that he's trying to give credit to the GNU project for making
     55 Linux useful.
     56 
     57 However, there's a lot of room for nuance here. For example, [12]Alpine Linux
     58 is a Linux distribution that uses [13]musl libc instead of [14]glibc and [15]
     59 busybox instead of GNU coreutils. It's still a Linux distribution, but it
     60 doesn't use the GNU toolset. It's still a Linux distribution, but it's not GNU/
     61 Linux.
     62 
     63 Mara is hacker
     64 <[16]Mara> Also, for the record you can build the Linux kernel with clang, but
     65 that's a whole other can of worms. For one, GCC supports many more targets than
     66 clang likely ever will, but in general there are some compromises you need to
     67 make until clang implements some GCC-specific compiler extensions a bit better.
     68 Google, Facebook, and a few other companies do run LLVM compiled kernels in
     69 production though, so it's probably closer to viable than you think. Especially
     70 if you use ChromeOS or Android.
     71 
     72 So, what is a Linux distribution? It's a collection of tools that make Linux
     73 useful. It's a collection of tools that make Linux into a complete system. It's
     74 not a "flavor" of Linux (though this conceptually can exist with alternative
     75 kernels like the Zen kernel patchset), it's a system that just so happens to
     76 make Linux useful.
     77 
     78 As a counter-argument, consider the reason why Linux runs on more devices
     79 worldwide than there are people: [17]Android. Android does use the Linux
     80 kernel, but it doesn't use any GNU tools in the stack at all. You can't take
     81 programs that are compiled against other Linux distributions and run them on
     82 Android. You can't take programs that are compiled against Android and run them
     83 on other Linux distributions.
     84 
     85 Aoi is wut
     86 <[18]Aoi> Wait, so does this mean Android's not a Linux distribution? What is
     87 it then?
     88 
     89 I'm going to argue that Android is not a Linux distribution unto itself.
     90 Android is a Linux implementation. It uses the Linux kernel, but that's where
     91 the similarities with the rest of the ecosystem end. Android is its own little
     92 world where there's just enough system tools to get the system running, but
     93 once you get into the UI, it's a completely different world. It's a completely
     94 different ecosystem. It's a completely different operating system.
     95 
     96 Aoi is wut
     97 <[19]Aoi> So what's the difference between a Linux distribution and a Linux
     98 implementation?
     99 Cadey is enby
    100 <[20]Cadey> It's a bit of a fuzzy line, but I'd say that a Linux distribution
    101 is a collection of discrete tools that make Linux useful, and a Linux
    102 implementation is a cohesive collection of bespoke tools that make Linux into a
    103 complete system. Really, you could argue that if it has /bin/sh, it's a Linux
    104 distribution.
    105 
    106 gokrazy
    107 
    108 [21]gokrazy is a Linux implementation that I've used off and on for a few years
    109 now. It's a very interesting project because everything on the system is
    110 written in Go save the kernel. The init process is in Go (and even listens over
    111 HTTP to handle updates!), every userland process is written in Go, and even the
    112 core system services are written in Go.
    113 
    114 Out of the box a gokrazy install comes with these basic tools:
    115 
    116   • The init process that is mandated to be the parent of all userland
    117     processes by the Linux kernel.
    118   • A [22]DHCP client that automatically configures the network interface.
    119   • A [23]NTP client that automatically sets the system clock.
    120   • A little tool to save randomness from the kernel to a file so that it can
    121     be used to seed the random number generator on boot (because the Raspberry
    122     Pi doesn't have a robust hardware random number generator)
    123 
    124 That's it. Everything else from the web UI to A/B update logic is written in
    125 Go. It boots in literal seconds, uses an insanely small amount of RAM out of
    126 the box, and runs with nearly zero overhead. When you configure your gokrazy
    127 install to run additional software, you do so by adding the Go command path to
    128 a configuration file and then updating to trigger a reboot into the new
    129 version.
    130 
    131 Here's an example of what my gokrazy virtual machine's file tree looks like:
    132 
    133 / # tree etc gokrazy user
    134 etc
    135 ├── breakglass.authorized_keys
    136 ├── gokr-pw.txt
    137 ├── gokrazy
    138 │   └── sbom.json
    139 ├── hostname
    140 ├── hosts
    141 ├── http-port.txt
    142 ├── https-port.txt
    143 ├── localtime
    144 ├── machine-id
    145 ├── resolv.conf -> /tmp/resolv.conf
    146 └── ssl
    147     └── ca-bundle.pem
    148 
    149 gokrazy
    150 ├── dhcp
    151 ├── heartbeat
    152 ├── init
    153 ├── ntp
    154 └── randomd
    155 
    156 user
    157 ├── breakglass
    158 ├── fbstatus
    159 ├── qemu-guest-kragent
    160 ├── serial-busybox
    161 ├── tailscale
    162 ├── tailscaled
    163 └── waifud-gok-agent
    164 
    165 That is the entire system. It's all stripped down to these few programs,
    166 configuration files, and one symlink for DNS resolution. This is a very minimal
    167 system, and it's all you need to run statically linked Go programs. It's very
    168 easy to deploy your own services to it too. It's probably the easiest platform
    169 I know of that lets you just deploy a Go binary and have it run as a service,
    170 automatically restarting when it crashes.
    171 
    172 The tooling
    173 
    174 When I used gokrazy back in the day, you had to use a command line called
    175 gokr-packer that you passed a bunch of command line flags to with information
    176 about all the Go programs you wanted to run on the machine, configuration for
    177 those programs, and any other meta-information like where the update tool
    178 should push the image to. It was a bit of a pain to use, but it worked.
    179 Recently the [24]gok tool was added to the project, and this has been
    180 revolutionary when it comes to using and administrating gokrazy installs.
    181 
    182 Essentially, gok is a wrapper around the existing gokr-packer logic with a JSON
    183 file to store your configuration details. It's a lot easier to use, understand,
    184 and automate. You don't have to remember command line flags or maintain
    185 unwieldy scripts. You just edit a JSON file and push updates with gok update.
    186 It's amazingly simple.
    187 
    188 Setting up a gokrazy machine
    189 
    190 As an example, I'm going to show you how to install a bunch of tailnet addons
    191 to a gokrazy machine. I'm also going to assume that you don't have a gokrazy
    192 install set up yet, so we'll need to install it. To do this, we'll need to do a
    193 few simple things:
    194 
    195   • Install the gok tool.
    196   • Create your gok configuration.
    197   • Install Tailscale on the machine.
    198   • Create your "seed" image with gok overwrite.
    199   • Boot it on your Raspberry Pi or VM.
    200   • Push any updates to the image to the machine with gok update.
    201 
    202 First, let's install the gok tool. In order to do this, you need to have the 
    203 [25]Go toolchain installed. Once you have that, you can run go install to
    204 install the gok tool:
    205 
    206 go install github.com/gokrazy/tools/cmd/gok@main
    207 
    208 Mara is hacker
    209 <[26]Mara> You may want to ensure that ~/go/bin is in your $PATH variable so
    210 that you can run it by the name gok instead of ~/go/bin/gok.
    211 
    212 Next, create a new gokrazy configuration with gok new:
    213 
    214 gok new -i casa
    215 
    216 This will create a configuration named casa (cf: Spanish for "house") in ~/
    217 gokrazy/casa. This is where all of your configuration files will live. You can
    218 edit the configuration file with gok edit:
    219 
    220 gok edit -i casa
    221 
    222 If you are making a virtual machine
    223 
    224 If you are making a virtual machine, you will need to override the kernel and
    225 firmware packages. You can do this by adding the following to your
    226 configuration file:
    227 
    228 {
    229   // ...
    230   "KernelPackage": "github.com/rtr7/kernel",
    231   "FirmwarePackage": "github.com/rtr7/kernel",
    232   // ...
    233 }
    234 
    235 You will need to prefix the gok overwrite and gok update commands with GOARCH=
    236 amd64 to ensure that Go builds x86_64 binaries instead of ARM binaries:
    237 
    238 GOARCH=amd64 gok update -i casa
    239 
    240 If you don't do this, you will get arm64 binaries being built. This may require
    241 manual recovery of your virtual machine.
    242 
    243 Let's make our lives easier by installing [27]Tailscale on the machine. By
    244 default, gokrazy will announce its hostname over DHCP, which usually makes most
    245 consumer routers pick it up and then lets you ping it by name. When you have 
    246 [28]MagicDNS enabled, Tailscale can take over this logic and prevent you from
    247 accessing the machine by name.
    248 
    249 However, Tailscale is written in Go and doesn't require any of the services
    250 that most Linux distributions provide in order to function. It's a perfect fit
    251 for gokrazy. You can install it with gok add:
    252 
    253 gok add tailscale.com/cmd/tailscaled
    254 gok add tailscale.com/cmd/tailscale
    255 
    256 And be sure to add the mkfs service to create a persistent partition on /perm:
    257 
    258 gok add github.com/gokrazy/mkfs
    259 
    260 Next, fetch an [29]auth key from [30]the admin console and make sure you check
    261 that it's reusable. Then, add the following to your configuration file under
    262 the PackageConfig block:
    263 
    264 {
    265     // ...
    266     "PackageConfig": {
    267         // ...
    268         "tailscale.com/cmd/tailscale": {
    269             "CommandLineFlags": [
    270                 "up",
    271                  // paste your key here!
    272                 "--authkey=tskey-auth-hunter2-hunter2hunter2hunter2"
    273             ]
    274         },
    275         // ...
    276     }
    277     // ...
    278 }
    279 
    280 Mara is hacker
    281 <[31]Mara> You can pass any other [32]tailscale up flags you want here, such as
    282 --advertise-exit-node if you want to use your gokrazy machine as an [33]exit
    283 node.
    284 
    285 This will make your machine automatically connect to Tailscale on boot.
    286 
    287 Next, we need to create our "seed" image with gok overwrite. First, figure out
    288 what the device node for your SD card is. On Linux, you can do this with lsblk:
    289 
    290 lsblk
    291 
    292 And then look for the one that has the same size as your SD card. In my case,
    293 it's /dev/sdd. Once you have that, you can run gok overwrite:
    294 
    295 gok overwrite --full /dev/sdd
    296 
    297 However if you want to write the image to a file (such as if you are doing mass
    298 distribution or making a VM image), you need to use gok overwrite with a file
    299 instead of a device node. This will create a 16 GB image:
    300 
    301 gok overwrite -i casa --full gokrazy.img --target_storage_bytes 17179869184
    302 
    303 Once you have your image, you can write it to your SD card with dd (or [34]
    304 balenaEtcher) or import it into your virtual machine hypervisor of choice.
    305 
    306 Once you have your image written to your SD card, you can boot it on your
    307 Raspberry Pi or VM.
    308 
    309 Aoi is wut
    310 <[35]Aoi> Wait, so how do I log in with a shell?
    311 Cadey is enby
    312 <[36]Cadey> You don't. gokrazy doesn't have a login prompt. It's a single-user
    313 system. There is [37]breakglass as a tool of last resort to modify things, but
    314 you only have a very minimal subset of busybox to work with, so it should be
    315 avoided if at all possible.
    316 
    317 Once you have your machine booted and it responds to pings over Tailscale, you
    318 can open its HTTP interface in your browser. If you called your machine casa,
    319 you can open it at [38]http://casa. It will prompt you for a username and
    320 password. Your username is gokrazy, and the password is near the top of your
    321 config.json file. When you log in, you'll see a screen like this:
    322 
    323 [39][gokrazy-ui]
    324 
    325 This is the gokrazy web UI. It lets you see the status of your machine and any
    326 logs that are being generated by your applications. You can also start, stop,
    327 and restart any of your applications from here. It's a very simple UI, but it's
    328 fantastic for debugging and monitoring.
    329 
    330 Tailnet addons
    331 
    332 Now that we have a Gokrazy system up and running, let's add some programs to
    333 it! I'm going to list a couple tailnet addons that give your tailnet
    334 superpowers. These are all written in Go, so they're a perfect fit for gokrazy.
    335 
    336 Today I'm going to show you how to install these tools into your tailnet:
    337 
    338   • [40]golink - a URL shortener at http://go
    339   • [41]tmemes - an internal meme generator you can host at http://memegen
    340   • [42]tclip - a pastebin you can host at http://paste
    341 
    342 These tools help you augment your tailnet by giving you tools that will make
    343 you and your team's life a lot easier. A URL shortener helps you link to
    344 complicated Google Docs URLs. A meme generator gives you a new innovative way
    345 to let off steam. A pastebin lets you share text with your team without having
    346 to worry about the service you're using going offline due to no fault of your
    347 own.
    348 
    349 golink
    350 
    351 To install golink, we need to add the golink binary to the configuration. You
    352 can do this with gok add:
    353 
    354 gok add github.com/tailscale/golink/cmd/golink
    355 
    356 Then configure it with gok edit:
    357 
    358 {
    359     // ...
    360     "PackageConfig": {
    361         // ...
    362         "github.com/tailscale/golink/cmd/golink": {
    363             "CommandLineFlags": [
    364                 "--sqlitedb=/perm/home/golink/data.db"
    365             ],
    366             "Environment": [
    367                 // the same one from before
    368                 "TS_AUTHKEY=tskey-auth-hunter2-hunter2hunter2hunter2"
    369             ],
    370             // don't start the service until NTP catches up
    371             "WaitForClock": true
    372         },
    373         // ...
    374     }
    375     // ...
    376 }
    377 
    378 And finally push it with gok update:
    379 
    380 gok update -i casa
    381 
    382 It'll build the image, push it out over Tailscale, trigger a reboot, and be
    383 back up in the span of a minute. Once it's back up, you can open the web UI
    384 again and see the status of your golink instance at [43]http://casa/status?path
    385 =%2fuser%2fgolink:
    386 
    387 [44][golink]
    388 
    389 And then you can start using short URLs at [45]http://go:
    390 
    391 [46][golink-ui]
    392 
    393 And that's it! You now have a super minimal VM running small programs that let
    394 you do useful things to you. You can add more programs to your configuration
    395 file and push them with gok update to add more functionality to your machine.
    396 You can even add your own programs to the configuration file and push them to
    397 your machine. It's a very simple system, but it's very powerful.
    398 
    399 tmemes
    400 
    401 Google is infamous for having an internal service named [47]memegen. This
    402 allows Googlers to make internal-facing memes about the slings and arrows that
    403 impact them as highly paid programmers. This is an internal service inside
    404 Google that has a lot of serious investment of time and energy to make it the
    405 best possible experience it can be. It's to the point that reportedly people
    406 can keep up with how an all-hands meeting is going by the tone of the sarcastic
    407 memes that are being posted to memegen.
    408 
    409 The main reason this is run inside Google is to avoid information leaking via
    410 memes. Yes, this is an actual threat model.
    411 
    412 Thanks to the magic of Tailscale, you can make your own private memegen using 
    413 [48]tmemes. tmemes is a tailnet addon that lets you post image macro templates
    414 and layer wisdom over it in the form of text.
    415 
    416 Here's an example meme:
    417 
    418 [49][society-if]
    419 
    420 To add tmemes to your gokrazy machine, you can use gok add:
    421 
    422 gok add github.com/tailscale/tmemes/tmemes
    423 
    424 Then open your config with gok edit and add the following to your PackageConfig
    425 block:
    426 
    427 {
    428     // ...
    429     "PackageConfig": {
    430         // ...
    431         "github.com/tailscale/tmemes/tmemes": {
    432             "Environment": [
    433                 "TS_AUTHKEY=tskey-auth-hunter2-hunter2hunter2hunter2"
    434             ],
    435             "CommandLineFlags": [
    436                 // change this to your desired hostname
    437                 "--hostname=memegen",
    438                 // change this to your username on Tailscale
    439                 "--admin=Xe@github",
    440                 "--store=/perm/home/tmemes"
    441             ],
    442             "WaitForClock": true
    443         },
    444         // ...
    445     },
    446     // ...
    447 }
    448 
    449 And then push it with gok update:
    450 
    451 gok update -i casa
    452 
    453 Then you can head to [50]http://memegen and upload a template to make your own
    454 dank memes.
    455 
    456 If you want to integrate your own tools with tmemes, you can check out the [51]
    457 API documentation. This should help you do whatever it is you want with a meme
    458 generator as a service.
    459 
    460 tclip
    461 
    462 Sometimes you just need a place to paste text and get a URL pointing to it. 
    463 [52]tclip is a tool that you can add to your tailnet and get exactly that. It's
    464 a very simple tool, but it's very useful. It's also written in Go, so it's a
    465 perfect fit for gokrazy. [53]Their recent update to remove Cgo dependencies
    466 makes it possible to run your tclip node on a gokrazy machine.
    467 
    468 To add tclip to your gokrazy machine, you can use gok add:
    469 
    470 gok add github.com/tailscale-dev/tclip/cmd/tclipd
    471 
    472 Then open your config with gok edit and add the following to your PackageConfig
    473 block:
    474 
    475 {
    476     // ...
    477     "PackageConfig": {
    478         // ...
    479         "github.com/tailscale-dev/tclip/cmd/tclipd": {
    480             "CommandLineFlags": [
    481                 "--data-location=/perm/home/tclip/"
    482             ],
    483             "WaitForClock": true,
    484             "Environment": [
    485                 "TS_AUTHKEY=tskey-auth-hunter2-hunter2hunter2hunter2",
    486                 "USE_FUNNEL=true" // Remove this if you don't want to use Funnel
    487             ]
    488         },
    489         // ...
    490     }
    491 }
    492 
    493 And then push it with gok update:
    494 
    495 gok update -i casa
    496 
    497 And then you can start using it by heading to [54]http://paste. Install the
    498 command-line tool on your development workstation with go install:
    499 
    500 go install github.com/tailscale-dev/tclip/cmd/tclip@latest
    501 
    502 Here's an example tclip link if you want to see what it looks like in practice:
    503 [55]interjection.c. It's a very simple tool, but it's very useful.
    504 
    505 Conclusion
    506 
    507 gokrazy is insanely cool. It's the easiest way to deploy Go services to your
    508 homelab. It integrates seamlessly with Tailscale, and is something that I'm
    509 very excited to see grow and mature. I'm very excited to see what the future
    510 holds for gokrazy, and I'm very excited to see what people do with it.
    511 
    512 I've seen signs that they're going to be adding an automatic update process,
    513 and that has me very excited. I'm also excited to see what other services
    514 people add to the gokrazy ecosystem. I'm hoping to add a few of my own in the
    515 future, and I'm hoping to see what other people do with it.
    516 
    517 Mara is hacker
    518 <[56]Mara> Spoiler alert: [57]waifud support is coming soon to a homelab near
    519 you.
    520 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    521 Share
    522 
    523 Facts and circumstances may have changed since publication. Please contact me
    524 before jumping to conclusions if something seems wrong or unclear.
    525 
    526 Tags: go, gokrazy, linux
    527 
    528 Copyright 2012-2024 Xe Iaso (Christine Dodrill). Any and all opinions listed
    529 here are my own and not representative of any of my employers, past, future,
    530 and/or present.
    531 
    532 Like what you see? Donate on [59]Patreon like [60]these awesome people!
    533 
    534 Served by xesite v4 (/nix/store/
    535 5pbrsn0khhga62ipyl043c9z22dlah4w-xesite_v4-20231122/bin/xesite) with site
    536 version [61]71d942d2 , source code available [62]here.
    537 
    538 
    539 References:
    540 
    541 [1] https://xeiaso.net/
    542 [2] https://xeiaso.net/blog
    543 [3] https://xeiaso.net/contact
    544 [4] https://xeiaso.net/resume
    545 [5] https://xeiaso.net/talks
    546 [6] https://xeiaso.net/vods
    547 [7] https://xeiaso.net/signalboost
    548 [8] https://xeiaso.net/characters#mara
    549 [9] https://gcc.gnu.org/
    550 [10] https://www.gnu.org/software/coreutils/coreutils.html
    551 [11] https://www.gnu.org/software/libc/
    552 [12] https://alpinelinux.org/
    553 [13] https://musl.libc.org/
    554 [14] https://www.gnu.org/software/libc/
    555 [15] https://en.wikipedia.org/wiki/BusyBox
    556 [16] https://xeiaso.net/characters#mara
    557 [17] https://en.wikipedia.org/wiki/Android_(operating_system)
    558 [18] https://xeiaso.net/characters#aoi
    559 [19] https://xeiaso.net/characters#aoi
    560 [20] https://xeiaso.net/characters#cadey
    561 [21] https://gokrazy.org/
    562 [22] https://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Protocol
    563 [23] https://en.wikipedia.org/wiki/Network_Time_Protocol
    564 [24] https://gokrazy.org/quickstart/
    565 [25] https://golang.org/doc/install
    566 [26] https://xeiaso.net/characters#mara
    567 [27] https://tailscale.com/
    568 [28] https://tailscale.com/kb/1081/magicdns/
    569 [29] https://tailscale.com/kb/1085/auth-keys/
    570 [30] https://login.tailscale.com/admin/settings/keys
    571 [31] https://xeiaso.net/characters#mara
    572 [32] https://tailscale.com/kb/1080/cli/#up
    573 [33] https://tailscale.com/kb/1103/exit-nodes/?q=exit%20node
    574 [34] https://etcher.balena.io/
    575 [35] https://xeiaso.net/characters#aoi
    576 [36] https://xeiaso.net/characters#cadey
    577 [37] https://github.com/gokrazy/breakglass
    578 [38] http://casa/
    579 [39] https://cdn.xeiaso.net/file/christine-static/blog/2023/gokrazy/gokrazy-ui.jpg
    580 [40] https://github.com/tailscale/golink
    581 [41] https://github.com/tailscale/tmemes
    582 [42] https://github.com/tailscale-dev/tclip
    583 [43] http://casa/status?path=%2fuser%2fgolink
    584 [44] https://cdn.xeiaso.net/file/christine-static/blog/2023/gokrazy/golink.jpg
    585 [45] http://go/
    586 [46] https://cdn.xeiaso.net/file/christine-static/blog/2023/gokrazy/golink-ui.jpg
    587 [47] https://www.buzzfeednews.com/article/reyhan/inside-googles-internal-meme-generator
    588 [48] https://github.com/tailscale/tmemes
    589 [49] https://cdn.xeiaso.net/file/christine-static/blog/2023/gokrazy/society-if-gokrazy.jpg
    590 [50] http://memegen/
    591 [51] https://github.com/tailscale/tmemes/blob/main/docs/api.md
    592 [52] https://tailscale.dev/blog/tclip
    593 [53] https://tailscale.dev/blog/tclip-updates-092023
    594 [54] http://paste/
    595 [55] https://paste.shark-harmonic.ts.net/paste/696b9b02-90ac-4adc-a33d-d749bb6f460f
    596 [56] https://xeiaso.net/characters#mara
    597 [57] https://github.com/Xe/waifud-gok-agent
    598 [59] https://patreon.com/cadey
    599 [60] https://xeiaso.net/patrons
    600 [61] https://github.com/Xe/site/commit/71d942d2664615d081d14037ad9930848f6a72b4
    601 [62] https://github.com/Xe/site