[{"content":"Sometimes if you just need some light web scraping, nothing too complicated. Other times, chrome webdriver will just decide to not work, but that\u0026rsquo;s pretty rare. Or you really need to extract some useful bits of information, but you don\u0026rsquo;t have selenium already installed handy. Maybe you\u0026rsquo;re on a borrowed laptop that resets on every checkout.\nIn these cases, the devtools console will do. No need to take out selenium.\nThe essence here is you can interact with the page in a few basic, but often good enough ways:\ndocument.querySelector, obviously outerHTML on HTMLElement to stringify a DOM node. Do some string manipulation to clean it up. optionally do some string or DOM processing (clone the node first) when done, copy the array to clipboard. Now you have scraped output! simulate user clicking with click() on HTMLElement This might not always work, so your milage may vary. study their code, and emulate some behaviors yourself to \u0026ldquo;interact\u0026rdquo; with the page For example, if button fetches HTML and replaces a subtree, you can just do the fetch yourself. Only really possible if it\u0026rsquo;s using jQuery or simple vanilla JS. for navigation\u0026hellip; Replace window.location.href to navigate, but be careful: this wipes out your devtools console. If you really need to do this, spending some time to craft a full script that you can just paste into console will pay off. Modify \u0026lt;a\u0026gt; with target=\u0026quot;_blank\u0026quot; to batch open links in new tabs. call setTimeout recursively This isn\u0026rsquo;t super easy to code. Consider quickly cobble together an async/await helper like https://stackoverflow.com/a/33292942 \u0026hellip;\n\u0026hellip;\n\u0026hellip;\nOk I\u0026rsquo;ll confess, the real reason is chrome webdriver broke on me. As they say: I did this not because it was easy, but because I thought it would be easy. Easier than selenium, which honestly I\u0026rsquo;m not sure if that\u0026rsquo;s true. I guess it was kind of fun?\n","permalink":"https://www.rtk0c.com/blog/poor-mans-selenium/","summary":"When chrome-driver just decides to not work on your development workstation today","title":"Poor man's selenium"},{"content":"Or: hand roll a ngrok with protonvpn port forwarding for shenanigans\nUpdated 2025-11-08: added addendum section on using wireguard in a network namespace.\nDumbness: port forward sshd Our general plan is this: Bring up a wireguard tunnel to protonvpn as usual. Bring up sshd as usual. Port forward some public port to 10.2.0.2:22, with natpmpc(1).\nThe first two steps are just the standard issue procedure, I won\u0026rsquo;t go over it here. I\u0026rsquo;ll assume the wireguard config is placed at /etc/wireguard/protonvpn.conf, so bringing it up is simply wg-quick up protonvpn. Also remember to harden sshd, like disable password login and setup fail2ban. Otherwise you\u0026rsquo;ll have a pretty bad day\u0026hellip;\nFirst, the port forwarding logic. It\u0026rsquo;s a simple bash script /usr/local/bin/protonvpn-update-port-mapping, adapted from here.\n#!/bin/bash TMPFILE=/tmp/natpmpc_output # We can assume the exit node IP doesn\u0026#39;t change ip=$(curl -s --interface=protonvpn -4 icanhazip.com) echo \u0026#34;$ip\u0026#34; \u0026gt; \u0026#34;$RUNTIME_DIRECTORY/public-ip\u0026#34; ip6=$(curl -s --interface=protonvpn -6 icanhazip.com) echo \u0026#34;$ip6\u0026#34; \u0026gt; \u0026#34;$RUNTIME_DIRECTORY/public-ip6\u0026#34; while true; do natpmpc -a 1 22 udp 60 -g 10.2.0.1 \u0026gt; /dev/null \\ \u0026amp;\u0026amp; natpmpc -a 1 22 tcp 60 -g 10.2.0.1 \u0026gt; $TMPFILE \\ || { echo -e \u0026#34;ERROR with natpmpc command \\a\u0026#34; break } port=$(grep \u0026#39;TCP\u0026#39; $TMPFILE | grep -o \u0026#39;Mapped public port [0-9]*\u0026#39; | awk \u0026#39;{print $4}\u0026#39;) rm $TMPFILE echo \u0026#34;$port\u0026#34; \u0026gt; \u0026#34;$RUNTIME_DIRECTORY/public-port\u0026#34; sleep 45 done In the script provided in protonvpn docs, natpmpc is called with privateport = 0, which means use the same port as the public port, allocated by the gateway. Changing it to 22 makes it so that the gateway still chooses whatever it wants, but the privateport is always mapped to 22, on which sshd is listening.\nYou\u0026rsquo;ll notice we map UDP on line 12, in addition to TCP on line 13. Indeed it\u0026rsquo;s not necessary for sshd, but it\u0026rsquo;s useful for something like reverse proxying a wireguard mesh. I haven\u0026rsquo;t done it, because tailscale and what not is vastly more convenient and versatile than this. I guess I will say the age-old punchline here: running wireguard on this is left as an exercise to the reader.\nSecond, a systemd service /etc/systemd/system/protonvpn-natpmp.service. Note wg-quick@.service is a template provided in the most linux distros just runs wg-quick on the pprovided config. And setting RuntimeDirectory will create the $RUNTIME_DIRECTORY env var used by the script above.\n[Unit] Description=ProtonVPN NAT-PMP port forwarding update After=wg-quick@protonvpn.service Requires=wg-quick@protonvpn.service [Service] Type=exec ExecStart=/usr/local/bin/protonvpn-update-port-mapping RuntimeDirectory=protonvpn-natpmp # Harden, because why not ProtectSystem=strict PrivateTmp=true ProtectHome=true PrivateDevices=true ProtectKernelTunables=true ProtectControlGroups=true [Install] WantedBy=multi-user.target Reload to pick up the new unit files. Enable and immediately start both services:\n# systemctl daemon-reload # systemctl enable --now wg-quick@protonvpn.service protonvpn-natpmp.service Read the public address under /run/protonvpn, now you should be able to ssh, from anywhere on the internet, to this machine.\nSilliness? port forward HTTP It\u0026rsquo;s basically the same process as for sshd. Make your HTTP server of choice, nginx or whatever, listen on 10.2.0.2:80 (or some other port of your choice). Maybe even setup DNS and a certificate for it. And then you can just access it on the public internet like any other server. That\u0026rsquo;s it.\nNow, please don\u0026rsquo;t do any nefarious things with this\u0026hellip; That\u0026rsquo;s how we lost port forwarding from mullvad. The curious reader who has not yet caught up on this matter may perform an internet search on their own behalf.\nAutomatically redirect to current port allocation Remembering that magic port number is pretty painful. Keeping it in links you save or send to friends is also pretty painful. Wouldn\u0026rsquo;t it be nice to have some kind of short url service, that just redirects you to the current IP:port assigned, as of accessing?.\nFor example, you can go to [https://sh.example.com/foo/bar], and be redirected to [https://funny-business.example.com:12345/foo/bar]. And when\nThe python server that generates the redirects, and exposes an API for updating the mappings for our natpmp script to work with.\n#!/usr/bin/python # example config file \u0026#34;\u0026#34;\u0026#34; server-1 i.example.com server-2 ii.example.com this-is-a-joke foo.example.com:6969 \u0026#34;\u0026#34;\u0026#34; import sys from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import urlparse, parse_qs config = {} config_path = sys.argv[1] PARENT_DOMAIN = \u0026#34;example.com\u0026#34; with open(config_path) as f: for line in f: line = line.strip() if not line or line.startswith(\u0026#39;#\u0026#39;): continue prefix, port = line.split(maxsplit=1) config[prefix] = port def update_config(prefix, port): config[prefix] = port with open(config_path, \u0026#39;w\u0026#39;) as f: for prefix, port in config.items(): f.write(f\u0026#34;{prefix} {port}\\n\u0026#34;) class RedirectHandler(BaseHTTPRequestHandler): def do_POST(self): p = urlparse(self.path) if p.path != \u0026#34;/-/config\u0026#34;: return q = parse_qs(p.query) update_config(q[\u0026#39;prefix\u0026#39;][0], int(q[\u0026#39;port\u0026#39;][0])) self.send_response(200) self.end_headers() def do_GET(self): # self.path[0] is always \u0026#39;/\u0026#39;, start at idnex 1 to ignore that split_pt = self.path.find(\u0026#39;/\u0026#39;, 1) if split_pt == -1: split_pt = len(self.path) prefix = self.path[1:split_pt] rest = self.path[split_pt:] if port := config[prefix]: self.send_response(302) self.send_header(\u0026#39;Location\u0026#39;, f\u0026#34;https://{prefix}.{PARENT_DOMAIN}:{port}{rest}\u0026#34;) self.end_headers() return self.send_response(404) self.end_headers() HTTPServer((\u0026#39;127.0.233.80\u0026#39;, 8046), RedirectHandler).serve_forever() Run it somehow. Perhaps as a Type=simple systemd service.\nAnd nginx config fragment, which should be dropped inside the http{} block. A very standard reverse proxy setup. Unrestricted GET, and basic_auth protected POST (and everything else) for updating the redirect mappings.\nupstream sh { server 127.0.233.80:8046; keepalive 2; } server { listen 443 ssl; listen [::]:443 ssl; server_name sh.example.com; location / { limit_except GET { auth_basic \u0026#34;POST restricted\u0026#34;; auth_basic_user_file /etc/nginx/sh.example.com-htpasswd; } proxy_pass http://sh; } } And update our port mapping script to send the newly obtained port to our redirects server.\n--- a/protonvpn-update-port-mapping.sh +++ b/protonvpn-update-port-mapping.sh @@ -8,6 +8,9 @@ ip6=$(curl -s --interface=protonvpn -6 icanhazip.com) echo \u0026#34;$ip6\u0026#34; \u0026gt; \u0026#34;$RUNTIME_DIRECTORY/public-ip6\u0026#34; +SERVICE_NAME=funny-business + +old_port=0 while true; do natpmpc -a 1 22 udp 60 -g 10.2.0.1 \u0026gt; /dev/null \\ \u0026amp;\u0026amp; natpmpc -a 1 22 tcp 60 -g 10.2.0.1 \u0026gt; $TMPFILE \\ @@ -21,5 +24,14 @@ echo \u0026#34;$port\u0026#34; \u0026gt; \u0026#34;$RUNTIME_DIRECTORY/public-port\u0026#34; + if [[ $port != $old_port ]]; then + echo \u0026#34;Port changed from $old_port to $port\u0026#34; + # Valid DNS names and digits don\u0026#39;t need any URL encoding to be standard compliant, so I\u0026#39;m using -d to be short + curl -X POST -u \u0026#39;YOUR_USER:YOUR_PASSWORD\u0026#39; https://sh.example.com/-/config \\ + -d \u0026#34;prefix=$SERVICE_NAME\u0026#34; \\ + -d \u0026#34;port=$port\u0026#34; + old_port=$port + fi + sleep 45 done Start everything, and now you can visit [https://sh.example.com/funny-business] for fun and profit.\nAddendum: split tunnelling with netns For those who are unfamilar: split tunnelling is to selectively route some traffic through the VPN but not others, usually discriminated by originating application.\nTraditionally, you would do this with ip rule on linux. Or you\u0026rsquo;d go really heavyweight, and just slap docker on the problem, by putting both the VPN connection (wireguard) and the application into containers. But it\u0026rsquo;s also possible to just use a network namespace. Docker is basically just this, but also namespacing pid/uts/time/mnt/user at the same time.\nTo actually do this, you basically have two choices:\nsetup the interface manually with ip(1) use a heavyweight champion like wg-netns. with the upshot of both being incompatible with wg-quick(1) config files. VPN providers almost always provide wg-quick config generators. Having to manually reformat that is pretty tedious.\nSo this is the part I present my own cobbled together shell script wg-reallyquick that emulates a subset of wg-quick behavior while adding it to a netns: https://gist.github.com/rtk0c/ae7e9aa29fa1a83ba02c7768f871b11c\nEnjoy.\nP.S. usage in the gist comments\nP.P.S. I know that name sucks. But it\u0026rsquo;s also kind of funny at the same time, so I kept it.\nCommentary on specific use cases BitTorrent client is the best use case I found. It has all these properties at the same time:\ntorrent clients often don\u0026rsquo;t expose enough network configurables you really want a kill stich only interacts with the internet (i.e. not with other programs on the system). As soon as any of these properties go away, netns (manually) can seem like it\u0026rsquo;s not worth the effort anymore.\nEvery time the isolated application needs to communicate with something else on your system, e.g. HTTP reverse proxy, you start to need many systemd-socket-proxyd, or upgrade to veth(4) interfaces. In my opinion, at this point you might as well just use containerization.\nIf the software allows you to bind to a specific interface, or even better, set fwmark on packets it creates, you can just use regular ip rule based routing. It\u0026rsquo;s so much simpler to setup and easier to understand.\nBut also, with multiple applications all needing to be split tunneled, ip rule based routing can become pretty convoluted, so in that case, netns can seem appealing again. So it\u0026rsquo;s all situational.\nSome extra friendly links The grandma of all such efforts is ostensibly Routing \u0026amp; Network Namespace Integration from the official wireguard website (note: the author did not fact check this statement), which explains the concept and general procedure better than I can, so go read it.\nThis blog is a nicely written walkthrough on the particular thing you may be wanting to do, that is, sailing in the high seas. It also has a section on how to expose the web UI of some application running inside a netns using systemd-socket-proxyd.\nAlternatively, you can use good old socat to proxy TCP traffic.\nLastly, for diving even deeper, this blog disects the source code to show how the ip netns add family of command works under the hood, using syscalls. Sorry it doesn\u0026rsquo;t go into kernel code.\n","permalink":"https://www.rtk0c.com/blog/hand-rolled-ngrok-over-protonvpn/","summary":"Or: hand roll a ngrok with protonvpn port forwarding for shenanigans","title":"I paid for the whole vpn, so I'm damn well going to use the whole vpn"},{"content":"This one is even lower effort than #2. Not even annotations for most because these recorded when I was on cellphone.\nOh, the very first bullet is good, heavily recommended read. Heck, I even bothered to type out that much text on my phone! Also, ok, I guess I did put in the effort to add link text, because otherwise this will literally be just a wall of links. Please appreciate that :D\nI should put in more effort in distinguishing between verbatim titles in link text, and own paraphrasing. Ugh, too much effort! Sorry!\nThe Eternal Mainframe Mainframe was never and still isn’t about the physical form, the racks and racks of metal upon which computers hung. It’s about inaccessibility (“priesthood”) and centralization. “If offline use becomes uncommon, then the great and the good will ask: “What are you hiding?” Aren’t all prophesies almost necessarily be slippery slopes? Talks of chance and reasonableness almost certainly fails when the scope is far far long in the future.\nSeries on tangents and meandering into epistemology\nRule Thinkers In, Not Out Chesterton’s Fence: A Lesson in Thinking Very good introduction to cryptography for application programmers\nComprehensive introduction to using DDD (the GDB frontend)\nCode Generation in Rust vs C++26\nHow Pi Almost Wasn’t\nThe internet 20 years ago, or so as people thought. I think this again is on HN frontpage at one point.\nFan subbing effects (and why it’s better than your netflix srt subs) for outsiders\nBlacksmithing and Lisp\nWhat is HDR? Fantastic article (and blog!) clarifying all the mysteries behind HDR, specifically wrt. photography (as opposed to rendering).\nChomsky\u0026rsquo;s (yes that one) critique on LLM\nSeries on investigation on Windows command line parsing\nThe wild west of Windows command line parsing How Command Line Parameters Are Parsed The radix 2^51 trick\nSomeone\u0026rsquo;s rambling on centering Good introductory ideas, but leaves something to be desired on the topic of actually designing iconography and dealing with centering. Also, the sweeping take on icon fonts is inappropriate (granting the point of text centric logic used by fonts is inapt for icons).\nhttps://www.hopefulmons.com/p/children-of-the-geissler-tube\n缺字危机：一本书背后有多少不存在的汉字？——「表意」文字体系与数字编码规则间的深层冲突 About encoding and designing fonts for ancient Chinese glyphs.\n装配线上的“麦门”打工人, 摘自虹线周刊\n","permalink":"https://www.rtk0c.com/blog/link-clearance/3/","summary":"From my phone","title":"Link clearance #3"},{"content":" https://dmitrykandalov.com/liveplugin This is from a while ago. Make your Intellij work more like Emacs. Sort of. I think Emacs die hard fans would stone me for saying this. Whatever.\nhttps://chao-tic.github.io/blog/2018/12/25/tls How does C11/C++11 thread_local work?\nhttps://jakearchibald.com/scratch/alphavid/ (older incarnation from 2009) https://jakearchibald.com/2024/video-with-transparency/ HN submission for Lottie reminded me of Firefox\u0026rsquo;s famous animating-SVG-with-CSS-transform trick, leading to a few searches, ending here.\nhttps://hacks.mozilla.org/2009/06/tristan-washing-machine/ Another one on the journey. \u0026lt;video\u0026gt; foreign element in SVG sounds pretty cool. Broken in latest Firefox. Not sure if it\u0026rsquo;s a removed feature, or the source video just no longer exists (I guess the latter because the mp4 file doesn\u0026rsquo;t seem to resolve).\nhttps://tumult.com/hype/ Alternative to After Effects? Found in the said HN comments.\nhttps://navysbir.us/n25_1/N251-024.htm#qa Found this while looking for a self-hosted, ACME-capable certificate authority. For installing one root cert (preferably with Name Constraint) and can get all homelab devices to use TLS. Without having to deal with things going in the CT. Anyways. Apparently US Navy has this public website for their purchasing programs?\nhttps://gitlab.com/eql/lqml/ Use QML for UI, Common Lisp for scripting (in place of writing javascript inside QML files). Seems to provide interop between javascript and cl? Doesn\u0026rsquo;t seem to be a replacement for C++ modules; no way to interact with the QXxx C++ classes.\nhttps://www.citationneeded.news/curate-with-rss/ RSS rise up! Not very interesting, but perhaps a good thing to have on hand to send to people just dipping their toes in. Or people who heard RSS, want to try, but doesn\u0026rsquo;t know.\n","permalink":"https://www.rtk0c.com/blog/link-clearance/2/","summary":"Emacs, SVG, video encodings, ACME, and Common Lisp. Also, use RSS (we have one too)","title":"Link clearance #2"},{"content":"Application systems have completely different tolerances than libraries. Chromium can take a completely bespoke build system, equipped with its own C++ code generator, vendoring every library that it tends to use. Interoperability to downstream projects is so far in the rearview mirror, that it may as well exist as its own operating system. Same goes for Emacs. The same goes for Blender.\nWhat we need for building large and complex application programs is not the 100-year language, it’s the 1-year language.\nFortunately, this space is not the barren wild west of technology. Great pioneers have already trodden down at least one path ahead of us. Consider Terra. It’s a fully dynamic, very flexible scripting language Lua equipped to generate high performance C (and C++) code. It’s not without its footguns and design inconsistencies, but it should prove very useful.\nThe utility of Chromium and Emacs comes precisely from its performant basis, equipped with a very scripting language. Customizing Chromium UI is very simple, you write HTML/CSS/JS automatically harnessing the underlying framework. Customizing Emacs is the same.\nBindings are evil. Bindings are a waste of the universe’s limited entropy. Bindings should only be summoned when it is absolutely necessary, when it is to bring external incantations into your own spell. It should /never/, ever to bind together parts of your own spell. We need to get rid of sol2 and boost.python and guile.\nThe better approach is to build the binding into the language implementation being bound to. Instead of building a bespoke C program that implements computational geometry and crazy fast rendering of said geometry, and building a Python library for manipulating geometries, and bindings which glue the Python onto the C. Instead of this, we should build Blender as a flexible program which generates its necessary fast C. And in the process of which the binding is placed into place, automatically.\nWe should build a glorified macro assembler for the modern age, and pack it along with the application.\nLua is the base interpreter. Terra is merely a Lua program that spits out low level code. Terra, or whatever our new thing is, should mix Lua into its milk and egg and truffles, and build it all. This way all the power of Lua shall be present for harness at its heart content.\nIn this sense, we shall build a new lisp, a new SBCL, for its output is not merely the image, but also itself. Its image includes not only the program but also its environment and the capability of producing said environment. Its language shall not be constricted to whatever arcane dialect of lisp, but rather any two language of our choosing. Our creator of godotengine shall not be free to choose from whatever his heart contents, from C to Pascal to Rust to Nim to Odin to Jai to Carp.\n—Sunday, 2025-08-24, from the dinner table. Edited 2025-09-01 for grammar and spelling.\nAddendum on the macro-assembler construction\nIt is obviously no trivial feat to design a macro language around an existing language. Conceivably, every target one may wish to choose will need a specially designed macro language. Yet some parts should inevitably remain the same. Quoting and quasiquoting, file structure organization (coarseness of our macro; does it produce functions at a time or files at a time?). These may be reusable. Value roundtripping, memory model, specific syntax needs tweaking.\nThe outer language in which the macro assembler is built needs special consideration too. Lua is probably a pretty good choice, given its apt as an embedded scripting system in the final application. It’s also good for being small. Perl and python and ruby may be good in all aspects except its desirability as an embedded script. Their execution semantics are somewhat complicated. JS would be a horrible choice for its semantic complexity (and thus implementation complexity). Also, characteristics of the available implementations (fast) makes it very desirable as an embedded script. So this may make it desirable despite its negatives. Janet, Guile (-scheme) would sort of defeat the whole purpose of not invoking lisp, so as to not scare off people in the first place.\n","permalink":"https://www.rtk0c.com/blog/on-the-1-year-language/","summary":"\u003cp\u003eApplication systems have completely different tolerances than libraries. Chromium can take a completely bespoke build system, equipped with its own C++ code generator, vendoring every library that it tends to use. Interoperability to downstream projects is so far in the rearview mirror, that it may as well exist as its own operating system. Same goes for Emacs. The same goes for Blender.\u003c/p\u003e\n\u003cp\u003eWhat we need for building large and complex application programs is not the 100-year language, it’s the 1-year language.\u003c/p\u003e","title":"On the 1-year language"},{"content":"A collection of troubleshooting notes, general tips and tricks, or personal thoughts on the CS 166 Information Security class taught by Mark Stamp at SJSU.\nPart of this, dealing with specific homework problems, is written with the intention of being a last-resort rescue manual. I only include information you need to get out of potential deep water. No solutions to hard problems. No hand holding, especially no \u0026ldquo;here is how you solve this problem\u0026rdquo;.\nThe other part, the tips and thoughts, is just that. I will not stop myself blabbering on for forever.\nRecommended Tools Hex editor I\u0026rsquo;m only listing two of my favorites here.\nHxD https://mh-nexus.de/en/hxd/\nA very light, Windows-only hex editor. I use it on my laptop for quick things. It definitely will suffice for this class.\nImHex https://imhex.werwolv.net/\nMuch more powerful, but heavier. It has built-in support for pattern matching, processing, disassembly, etc. It also just looks really really nice, 200% eye candy factor.\nC compiler Some assignments supply C source code that you\u0026rsquo;ll have to compile. In general, they are not compatible with MSVC (Visual Studio), so special care needs taken on Windows.\nmacOS: install Xcode Command Line Tools, which contains an Apple-flavored clang. Alternatively, install either clang or gcc from Homebrew. Linux: your distro\u0026rsquo;s gcc will do. Or clang if you like that. Honestly if you use Linux why are you even reading this section, go away. :P Windows: install some flavor of gcc I highly recommend https://nuwen.net/mingw.html It’s tiny, just a zip file. Unzip it, you get a open_distro_window.bat, which when opened gives you a terminal with everything setup. There is zero room for PATH to go wrong. Otherwise, https://www.msys2.org/ What\u0026rsquo;s MinGW and what does it have to do with msys2 and cygwin In short, gcc is to MinGW as Linux is to distros.\ngcc is a bunch of code that can turn C source code, among other things, into an executable. It\u0026rsquo;s designed to run on various *nix platforms.\nMinGW is a bunch of extra code on top of gcc to make it (1) run on Windows, and (2) produce Windows (\u0026ldquo;PECOFF\u0026rdquo;) executable.\nBoth projects are just code, they don\u0026rsquo;t provide downloads (\u0026ldquo;builds\u0026rdquo;). For gcc, various Linux distros compile them and ship it as a package. For MinGW, these projects do the same job:\nhttps://winlibs.com/ https://github.com/niXman/mingw-builds-binaries https://cygwin.com/ This one tries to emulate the *nix environment on Windows. Comes with quite a few extra programs like bash. See google. https://www.msys2.org/ This one builds a bunch of software in addition to MinGW: bash, make, etc. It\u0026rsquo;s quite complicated, due to those software requiring compatibility layers like cygwin to function. I won\u0026rsquo;t explain here. See google. etc. Regardless of which one you download, you get a copy of gcc (and MinGW). The difference is the default configs, and the extra software they ship with gcc.\nIn fact, you\u0026rsquo;ll hear people call these things \u0026ldquo;MinGW distros\u0026rdquo;. The MinGW project has a list of notable distros.\nI would have linked something instead of writing this myself, but I literally can\u0026rsquo;t find anything of good quality on the internet\u0026hellip;\nx86 Static Analysis Some assignments ask you to disassemble and understand what a program is doing. For this—as is explained in Chapter 12 of the 3rd ed. textbook—you will need a disassembler that turns the bytes in the executable into assembly code.\nI am talking specifically about the offline disassembly and analysis functions. They also do debugging.\nI recommend Ghidra because that\u0026rsquo;s what I use. Realistically, for the things you\u0026rsquo;ll do in this class, either choice will work just fine. Similarly, comments below are targeted to use for this class. They\u0026rsquo;re shallow on purpose.\nGhidra. Completely free and open-source. Looks kind of ugly but once you get over that and interaction logic, it\u0026rsquo;s good.\nIDA Pro. Slightly closer eye candy. The built-in pattern matching works slightly better. Lots of people like it, so it (must also be) good. This costs money, there is a free version, but it doesn\u0026rsquo;t come with a decompiler\u0026hellip; not good.\nAccording to legend, all who have passed the great challenge will be rewarded a completely legitimate, legal way of using IDA Pro. Let the brave thus sail forth.\nBinary Ninja. Apparently this is a thing, so I\u0026rsquo;m including it here for completeness, never used, never heard until today, no remarks.\nNone of these are intuitive, so please consult the respective manuals and YouTube tutorial videos copiously. I shall not provide any guidance here because this blog will turn into the thickness of Critique of Pure Reason or something.\nx86 Dynamic Analysis Do note that all the tools above can also do debugging. Perfectly capable for the job for this class.\nI really like x64dbg (which does both x86_32 and x86_64). For reference, the 3rd edition of the textbook recommends OllyDbg (32-bit only), but it doesn\u0026rsquo;t receive updates anymore, so it won\u0026rsquo;t be as much of a transferable skill. It\u0026rsquo;ll still work great though.\nJava Decompiler As of the writing of this blog, I shall claim FernFlower is the best Java decompiler available.runsdon\u0026rsquo;t slap me don\u0026rsquo;t slap me\nAdmittedly, I\u0026rsquo;m biased because I worked on Minecraft modding for a while, and FernFlower is what the whole community settled on\u0026hellip; anyways!\nIt\u0026rsquo;s bundled in IntelliJ IDEA. You can just open any .class file, and it will decompile.\nIt\u0026rsquo;s also available as a CLI tool. If you\u0026rsquo;re going this route, consider using one of the forks that grew from the Minecraft modding efforts, such as Vineflower. It\u0026rsquo;s not going to matter for whatever you\u0026rsquo;ll be doing in this class, but support their efforts!\nChapter 5 Problem 4 - hash collision Hint: the expected answer is not the exact solution. That\u0026rsquo;s way too complicated\nI did end up in quite a rabbit hole trying to find the exact solution. These are not relevant to the textbook at all, but they\u0026rsquo;re still interesting reads:\nhttps://math.stackexchange.com/questions/407307/second-pair-of-matching-birthdays https://math.stackexchange.com/questions/2313215/birthday-paradox-2-pairs (same thing, just less general and less verbose) https://math.stackexchange.com/questions/1539271/probability-of-exactly-two-pairs-share-a-birthday-and-each-pair-shares-differen More hint It\u0026rsquo;s the square root approximation that’s implied, but much glossed over in the textbook. Fuller explanation here: https://en.m.wikipedia.org/wiki/Birthday_problem#Square_approximation\nProblem 24 - MD5 collision I\u0026rsquo;m getting different hashes The messages are supposed to be binary files, but the textbook gave them in hex codes. You probably need something like xxd -r -p, or PowerShell, or your hex editor of choice, to turn it into a binary message.\nDetails Something like these will work:\n$ cut -d \u0026#39; \u0026#39; -f 2- \u0026lt;\u0026lt;\u0026#39;EOF\u0026#39; | xxd -r -p \u0026gt; msg1.bin 00000000 d1 31 dd 02 c5 e6 ee c4 69 3d 9a 06 98 af f9 5c 00000010 2f ca b5 87 12 46 7e ab 40 04 58 3e b8 fb 7f 89 00000020 55 ad 34 06 09 f4 b3 02 83 e4 88 83 25 71 41 5a 00000030 08 51 25 e8 f7 cd c9 9f d9 1d bd f2 80 37 3c 5b 00000040 96 0b 1d d1 dc 41 7b 9c e4 d8 97 f4 5a 65 55 d5 00000050 35 73 9a c7 f0 eb fd 0c 30 29 f1 66 d1 09 b1 8f 00000060 75 27 7f 79 30 d5 5c eb 22 e8 ad ba 79 cc 15 5c 00000070 ed 74 cb dd 5f c5 d3 6d b1 9b 0a d8 35 cc a7 e3 EOF $ cut -d \u0026#39; \u0026#39; -f 2- \u0026lt;\u0026lt;\u0026#39;EOF\u0026#39; | xxd -r -p \u0026gt; msg2.bin 00000000 d1 31 dd 02 c5 e6 ee c4 69 3d 9a 06 98 af f9 5c 00000010 2f ca b5 07 12 46 7e ab 40 04 58 3e b8 fb 7f 89 00000020 55 ad 34 06 09 f4 b3 02 83 e4 88 83 25 f1 41 5a 00000030 08 51 25 e8 f7 cd c9 9f d9 1d bd 72 80 37 3c 5b 00000040 96 0b 1d d1 dc 41 7b 9c e4 d8 97 f4 5a 65 55 d5 00000050 35 73 9a 47 f0 eb fd 0c 30 29 f1 66 d1 09 b1 8f 00000060 75 27 7f 79 30 d5 5c eb 22 e8 ad ba 79 4c 15 5c 00000070 ed 74 cb dd 5f c5 d3 6d b1 9b 0a 58 35 cc a7 e3 EOF $ md5sum msg1.bin msg2.bin FYI, cut is used to strip the address column from the string.\nProblem 39 - stenography Getting a blank PDF on Windows The given stegoRead.c and stego.c is using fopen(2) in text mode, and CRT on Windows may expand byte sequence 0A (\\n) to 0D 0A (\\r\\n). I\u0026rsquo;m honestly not sure when it decides to do that.\nAdd b to the mode of all instances of fopen(...). For example, change fopen(outfname, \u0026quot;w\u0026quot;) into fopen(outfname, \u0026quot;wb\u0026quot;)\nChapter 12 Remember to use your favorite search engine to learn. The internet exists for a reason.\nYour OS All the programs provided here are Windows PECOFF executables.\nIf you have macOS/Linux, they should all work just fine in Wine (or variants it thereof). You may also grab a Windows VM to run them if Wine doesn\u0026rsquo;t work somehow. Do consider using an under version, e.g. XP or 7 just so it\u0026rsquo;s lighter on the resource usage. (Remember to disconnect internet to the VM if you are using an old Windows!)\nx86 assembly You\u0026rsquo;ll need a basic understanding of x86 32-bit assembly for this chapter. Very little is required, so whatever you know above another assembly should be transferable.\nGeneral tip 0: x86 assembly has 2 syntax flavors, AT\u0026amp;T and Intel. Internet resources may use either, just beware. GCC and whatnot by default produces AT\u0026amp;T syntax (🤮); if you see lots of % everywhere, or things like movq it\u0026rsquo;s this. The textbook and all the reverse engineering tools use Intel syntax (💖 as they should); if you see square brackets [rip+32h] or bare mov\u0026rsquo;s, it\u0026rsquo;s this.\nGeneral tip 1: je/jz and jne/jnz are the same instructions, just different mnemonics. You can always replace either with an unconditional jmp in place, they have the same encoding length.\nGeneral tip 2: test reg1,reg2 means taking a bitwise AND, and set zero/carry flags accordingly. xor reg1,reg1 is a convenient, 2 byte instruction that zeros any register.\nGeneral tip 3: almost all the string literals are contained in the .rdata section.\nGeneral tip 4: rip is the instruction pointer. Your debugger probably has a \u0026ldquo;set rip here\u0026rdquo; function to jump around.\nThe program just exits immediately after I type something Basically, when you double-click to open a .exe that\u0026rsquo;s a Console program, Windows only keeps the terminal open for as long as the program is running. Since the program exits right after it prints the last thing, it\u0026rsquo;ll \u0026ldquo;exit immediately after I type something\u0026rdquo;.\nThe proper way to do this is open a Cmd or PowerShell window, run the .exe from inside like path/to/my/program.exe. This is exactly the same thing as running a command-line program on macOS or Linux: you open Terminal.app, Konsole, Gnome Terminal or whatever, and type /path/to/my/program\nTo save you some head scratching: Note that in PowerShell supports cd D:/path/to/my/folder directly, but Cmd you have to type D: on its own to switch drive, followed by a separate command cd D:/path/to/my/folder to change directory in that drive.\n","permalink":"https://www.rtk0c.com/blog/cs166-tips-tricks/","summary":"\u003cp\u003eA collection of troubleshooting notes, general tips and tricks, or personal thoughts on the CS 166 Information Security class taught by Mark Stamp at SJSU.\u003c/p\u003e\n\u003cp\u003ePart of this, dealing with specific homework problems, is written with the intention of being a last-resort rescue manual. I only include information you need to get out of potential deep water. No solutions to hard problems. No hand holding, especially no \u0026ldquo;here is how you solve this problem\u0026rdquo;.\u003c/p\u003e","title":"Tips and tricks: CS 166 Information Security taught by Mark Stamp"},{"content":"Watch progress websites exist for anime and film. They work great. Socialization is great.\nBut they don\u0026rsquo;t record the exact time at which I finished each episode. I find such statistics amusing to dig through in some kind of year-end review. I also found it tremendously helpful to know which episodes were most recently watched, and in what order. Helps with recollecting the context of each show, especially when chasing more than a couple of shows at the same time. Depending on your judgment, using Letterboxd and MyAnimeList may also constitute giving private information to 3rd parties.\nSo I ended with an organization system that\u0026rsquo;s, effectively, top layer of bullet points for the show, and inner layer for the episodes. Something like:\n* STRT SHIROBAKO ** [2025-04-01 Tue 11:11] E01 ** [2025-04-16 Wed 22:22] E02 * TODO Do It Yourself!! * DONE ゆるキャンプ ** [2025-03-20 Thu 20:21] E10 ** [2025-03-21 Fri 20:59] E11 ** [2025-04-14 Mon 21:36] E12 * STRT mono ** [2025-04-13 Sun 16:56] E01 I want to be able to run a command, and get it as a linear timeline:\n* [2025-03-20 Thu 20:21] ゆるキャンプ E10 * [2025-03-21 Fri 20:59] ゆるキャンプ E11 * [2025-04-01 Tue 11:11] SHIROBAKO E01 * [2025-04-13 Sun 16:56] mono E01 * [2025-04-14 Mon 21:36] ゆるキャンプ E12 * [2025-04-16 Wed 22:22] SHIROBAKO E02 Which turns out to be a pretty easy simple regex-based forward parsing algorithm. I thought about using org-element-map, but it seemed like a bit of an overkill?\nTo use, I do g g M-x org-category-\u0026gt;timeline (evidently, I use evil-mode), bringing me to a new buffer *Category Timeline* filled out appropriately.\n(defun org-category-\u0026gt;timeline () \u0026#34;Collect category-timestamp tree into a single, chronologically ordered timestamp list. Starting at point.\u0026#34; (interactive) (let ((pt (point)) (curr-category nil) (curr-pt (search-forward \u0026#34;\\n* \u0026#34; nil t)) (items \u0026#39;())) ;; Collect items (while curr-pt (goto-char curr-pt) ;; Skip org-todo marker (forward-word) (forward-char) (setq curr-category (buffer-substring-no-properties (point) (line-end-position)) curr-pt (save-excursion (search-forward \u0026#34;\\n* \u0026#34; nil t))) (while-let ((i (re-search-forward (rx \u0026#34;\\n** [\u0026#34; (group (* (not \u0026#34;]\u0026#34;))) \u0026#34;] \u0026#34;) curr-pt t))) (let ((time (match-string-no-properties 1)) (comment (buffer-substring-no-properties (point) (line-end-position)))) (push (list (encode-time (org-parse-time-string time)) time curr-category comment) items)))) ;; Sort by time (sort items (lambda (a b) (time-less-p (car a) (car b)))) ;; Restore to starting point in the source buffer (goto-char pt) ;; Print items into a new buffer (switch-to-buffer \u0026#34;*Category Timeline*\u0026#34;) (erase-buffer) (dolist (i items) (let ((time (cadr i)) (category (caddr i)) (comment (cadddr i))) (insert \u0026#34;* [\u0026#34; time \u0026#34;] \u0026#34; category \u0026#34; \u0026#34; comment ?\\n))))) ","permalink":"https://www.rtk0c.com/blog/org-mode-watchlist/","summary":"\u003cp\u003eWatch progress websites exist for \u003ca href=\"https://myanimelist.net\"\u003eanime\u003c/a\u003e and \u003ca href=\"https://letterboxd.com\"\u003efilm\u003c/a\u003e. They work great. Socialization is great.\u003c/p\u003e\n\u003cp\u003eBut they don\u0026rsquo;t record the \u003cem\u003eexact time\u003c/em\u003e at which I finished each episode. I find such statistics amusing to dig through in some kind of year-end review. I also found it tremendously helpful to know which episodes were most recently watched, and in what order. Helps with recollecting the context of each show, especially when chasing more than a couple of shows at the same time.\nDepending on your judgment, using Letterboxd and MyAnimeList may also constitute giving private information to 3rd parties.\u003c/p\u003e","title":"Watchlist × Emacs org-mode"},{"content":"A short piece for teaching continuations, in the Platonic dialectic style. Whether it is helpful is for you to decide. Cheers to burritos!1\nTheofanis: Are you free right now, Asimoula?\nAsimoula: Well, surely if your matter of attention is short and concise, I shall give mine too regardless; and if it is long, we shall look upon its intriguability, and then making a decision.\nTheofanis: Well, to the very honest, it goes as such: this \u0026ldquo;continuation\u0026rdquo; thing has been perplexing me for days on end. Some state it very short simple, \u0026ldquo;a stored and resuamble state of computation\u0026rdquo;, but do not seem all that useful, or hint at why at all someone should use it. Others give very length examples,\n\u0026gt; (define cont \u0026#39;()) \u0026gt; (+ (/ 12 2) (* (+ 1 1) (call/cc (lambda (cc) (set! cont cc) 4)))) ;; As if the (call/cc) part is just 4, returned by the lambda 14 \u0026gt; (cont 1) ;; As if we\u0026#39;ve evaluated the expression *again*, but the (call/cc) is just 1 8 \u0026gt; (cont 2) 10 \u0026gt; (cont 4) 14 which is indeed fascinating, though now how it works and why seem to hide themselves even further in the thick fog. Calling them setjmp/longjmp makes perfect and clear sense, but at the cost of summoning Satanic monster who blasts putrid breathe all over my face2. What\u0026rsquo;s more, everybody seems so inclined to mention this thing called \u0026ldquo;continuation-passing style\u0026rdquo;, which are mostly followed by some nonsensical rewriting of perfectly fine programs to pass callbacks everywhere.\nAsimoula: A length topic indeed. But nonetheless an interesting one, so let us go through it! So I think I\u0026rsquo;ll do this: that example you cited is good enough at demonstrating its power, and so hereby I too will give a short and simple, but foggy and slimy statement: continuations are values (objects, if you like that word) that store a callstack, that you can jump back to while passing in some values.\nTheofanis: \u0026hellip;sure?\nAsimoula: We should now then take a detour out of mathematics and theory land, and dive into the dirty waters of real CPUs and implementations. After that, we\u0026rsquo;ll climb on mountain of asynchronous programming, so we can see a different way for using continuations—at the same time, so we can look at it from a different, perhaps much higher vantage point. In this way, we\u0026rsquo;ll not be blinded by the great city of Scheme and its fortress of walls, and be able to see the cloud of continuation in its full shape.\nBut enough babbling about, let\u0026rsquo;s jump in:\nAs we know, in assembly land, function calls happen through the callstack and a few registers. Inside some place, let\u0026rsquo;s say the function foo(), we decide to call another function bar(), so we push all the parameters onto the same, and then jump to the first instruction of bar().\nThe Callstack +----------------+ | \u0026lt;variables\u0026gt; | --+ | \u0026lt;return value\u0026gt; | |-- frame for bar() | foo(): 0xA3 | \u0026lt;- return address --+ +----------------+ | \u0026lt;variables\u0026gt; | --+ | \u0026lt;return value\u0026gt; | |-- frame for foo() | main(): 0x1E | \u0026lt;- return address --+ +----------------+ | . | | . | | . | Theofanis: Yes, that seems to make sense, now that you talk about. I do seem to recollect about this. Hesitating, for he being a JavaScript programmer by trade doesn\u0026rsquo;t quite have the C model on top of the head\nAsimoula: And so as you see, when we return from bar(), for hypothetically, we place the return value at a predetermined location, and read the \u0026ldquo;return address\u0026rdquo; from another predetermined location, and jmp to it. The frame of bar() is now free rein for anybody else to write on top of.3 In particular that return address points to a special chunk inside the assembly of foo(), that takes care of things after calling bar(). But the details are unimportant for us right now.\nTheofanis: Right.\nAsimoula: Now if you take your hand, and cover up the part of the stack for variables of bar() and its babbage, and squint your eyes a little bit, so might realize a magical thing that seems to be happening here: for all we know, Deina who works on bar() has just written a few bytes into \u0026lt;return value\u0026gt; and jmped to another address at [rsp+4], and he seems to have magically teleported to a place where the blinking lights and whistling crowd resumes into motion, in middle of foo().\nTheofanis: That does seem sort of magical, if you put such metaphors on top of it.\nAsimoula: Right— all I\u0026rsquo;m really saying is, by doing these two things—reading and writing a few values to a fixed location (relative to the stack pointer, of course), and jmping, we can in essence resume the state of execution to an arbitrary place in time.\nTheofanis: That is right, but do not see how it is useful. How does that make function calls special?\nAsimoula: This is the crucial behavior of continuations that make them both theoretically and practically powerful. They are a single vocabulary to describe function calls (replacing the stack upwards4), exceptions (replacing the stack downwards to the handler), generators (restoring the stack to inside the generator function, and then restoring the stack to the caller), coroutines (same as generators), or even goto! goto within a function is just replacing the stack to some state of the same function associated with some line number. So many more, countless uses.\nNow, what I just described in its full glory is not without simplifications compared to the machinery which Scheme offers. Yet nor is Scheme call/cc the true continuation. It is merely one physical incarnation of the concept. It is but rather the law of universal gravitation incarnation to the much more fundamental pattern that is inverse squares in nature.\nIf you do intend to learn to wield Scheme\u0026rsquo;s full power, sit down, grab a cup of tea (or coffee) and learn from the docs and examples. And documentation. And actual code. But\u0026hellip; I digress.\nOf course, power does not equate to usefulness, and as you can see: it\u0026rsquo;s a heavyweight vocabulary to be wielding around. In fact, it\u0026rsquo;s so powerful that most people find it too flexible.\nTheofanis: Now I see why these people are excited over it. For sure this demonstrates their power beautifully, Asimoula, but now can you also tell me what does these lovely continuation have to do with the \u0026ldquo;continuation-passing style\u0026rdquo;?\nAsimoula: Look at this ordinary program:\n(define (add a b) (+ a b)) ;; evaluate `add` first, then evaluate `display` (let ((res (add 40 2))) (display res)) ;; =\u0026gt; prints 42 compare that with:\n(define (add a b cont) (cont (+ a b))) ;; `cont` is the thing (i.e. `display) we want to eval after `(add 40 2)` (let (cont (lambda (res) (display res))) (add 40 2 cont)) ;; =\u0026gt; also prints 42 compare that with:\n;; same `add` definition (define (add a b cont) (cont (+ a b))) ;; Similarly, right outside of `call/cc` is the thing we want to eval after `(add 40 2)` (print (call/cc (add 40 2))) Both latter examples are using continuations. Both of them do functions by using CPS. I shall reiterate: call/cc is but a specific incarnation of the general idea of continuations.\nTo that idea, CPS is a property that some programs have. CPS can be obtained either by voluntarily writing it like so, or by applying a mechanical transformation to a regular program. Why have programs in CPS? Loosely, it\u0026rsquo;s easier to write algorithms transforming them in useful ways, i.e. optimizations.\nTheofanis: I see. So call/cc is to continuations as Java @FunctionInterface is to first-class functions. Or as C++ templates5 is to parametric polymorphism.\nAsimoula: You got it exactly right, my friend.\nAs some bonus chatter, a naive call/cc machinery by stack copying can be quite slow. It would literally copy the entire execution stack, megabytes of data, to the heap, and replace it when reentering the continuation. Non-naive implementations like Chicken exists, by rewriting the entire program to be CPS. This way, call/cc is free because literally everything is already a continuation. But this comes at the tradeoff that everything, even the code that does not explicitly use continuations, are just a little bit slower.\nAn naive call/cc machinery can be quite slow, by literally writing megabytes of data to replace the stack. Non-naive implementations like Chicken exists, but with tradeoffs like making everything, even the code that does not explicitly use continuations just a little bit slower.\nIf you, the reader, is really coming here confused, continuing may cause you to get more confused. The author is in fact aware of the burrito monads fallacy, and has no intention to stop. The author just found writing this amusing. Read on at your own risk (or benefit).\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nWhat I mean by this is setjmp/longjmp traditionally have a bad reputation among programmers, that it allows non-local control flow (\u0026ldquo;goto considered harmful\u0026rdquo;). Also this metaphor isn\u0026rsquo;t all that helpful to showing what continuations are supposed to enable; C programs really don\u0026rsquo;t use setjmp/longjmp similarly to continuations at all. I guess a little bit?\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nThese are all made up for the convenience for the demonstration. I don\u0026rsquo;t remember if any real calling conventions work in this exact way, but even if they do, things like parameters, register spillage, and stack pointer handling are omitted here. Don\u0026rsquo;t take it too seriously.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nUsually, the callstack is said to grow downwards (lower address means deeper callstack), based on that most calling conventions do it this way. I am going to call it \u0026ldquo;upwards\u0026rdquo; because it probably makes more sense to more people.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nIgnoring the template specializations part. I know that\u0026rsquo;s ad-hoc polymorphism, but that\u0026rsquo;s not the point.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://www.rtk0c.com/blog/on-continuations/","summary":"\u003cp\u003eA short piece for teaching continuations, in the Platonic dialectic style. Whether it is helpful is for you to decide. Cheers to burritos!\u003csup id=\"fnref:1\"\u003e\u003ca href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e1\u003c/a\u003e\u003c/sup\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e\u003cstrong\u003eTheofanis:\u003c/strong\u003e Are you free right now, Asimoula?\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAsimoula:\u003c/strong\u003e Well, surely if your matter of attention is short and concise, I shall give mine too regardless; and if it is long, we shall look upon its intriguability, and then making a decision.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTheofanis:\u003c/strong\u003e Well, to the very honest, it goes as such: this \u0026ldquo;continuation\u0026rdquo; thing has been perplexing me for days on end. \u003ca href=\"https://en.wikipedia.org/wiki/Continuation\"\u003eSome\u003c/a\u003e state it very short simple, \u0026ldquo;a stored and resuamble state of computation\u0026rdquo;, but do not seem all that useful, or hint at why at all someone should use it. Others give very length examples,\u003c/p\u003e","title":"On continuations"},{"content":"This series isn\u0026rsquo;t periodical at all. It\u0026rsquo;ll come out whenever I have enough of them accumulated over time.\nFamous hotel signs. If I recall correctly this is linked from a Hacker News discussion on\u0026hellip; something linguistics. The rest of the posts on this site are equally funny though.\nMozart2 and Clean. Two languages linked from The Lisp Curse post for containing novel and desirable features, being not Lisps (and thus not the /most powerful thing ever/) yet still everyone can learn from.\nTwo discussions on parsing from the Oils shell project.\nFor the ambitious compiler writers who like to reuse their parsing infrastructure for editor tooling as well. See the HN and reddit links, there are some interesting back and forth in there too.\nA few other descendent links that may be interesting and hard to find:\nPython\u0026rsquo;s pgen. On context-sensitivity. Some sort of omni-compiler-framework? bnfc Computer Science - Brian Kernighan on successful language design - YouTube A rather interesting project that tries to do what my WinXInputEmu does, but with remote thread code execution to patch out system functions instead of doing DLL hijacking. Looks much more featureful and much /much/ more complicated (likely unnecessarily).\nPair of twin articles on libraries and open access.\nAll publishers (money!!?!?) and libraries (out of control) and readers (difficulty in accessing) hates electronic journals. A different route: stop doing the storage, stop trying to emulate physical paper electronically — produce PDF or whatever, send to customers, done. Let the libraries take care of the website and everything just like how they take care of printed materials. Maintain a minimal internal archive, suddenly a lot of cost and product leakage_ concerns are gone.\nOne notable descendant, what are libraries for (out of the many in there):\nFor what is anything but a tool?\nYet another pamphlet (and fortunately, not a treatise) on dependencies.\n","permalink":"https://www.rtk0c.com/blog/link-clearance/1/","summary":"Unfortunately I probably won\u0026rsquo;t ever be as good as The Old New Thing","title":"Link clearance #1"},{"content":"TL;DR: kitty uses a custom terminfo xterm-kitty. Vim doesn\u0026rsquo;t like it. If you\u0026rsquo;re in a pinch, commit a crime, and hopefully it works fine. If you\u0026rsquo;re not, switch to another terminal for vim, or switch to neovim, or attempt to teach vim to speak kitty.\nIf you use Vim in kitty, local machine or going through SSH, and (at least) one of these is happening:\nPaste Ctrl+Shift+V from system clipboard is egregiously slow. Like two lines per second slow1. Paste is glitchy. All the whitespace get eaten, nowhere to be seen. Lines get jumbled together, parts of the clipboard overwrite another, etc2. kitty tells you your clipboard contains terminal escape sequences. Except it absolutely does not. Pasting elsewhere, still in kitty, like into bash or nvim works completely fine2. then congratulations, you have just discovered that Vim isn\u0026rsquo;t very compatible with kitty as a terminal emulator. Instead of trying to poorly summarize why, you can instead read the problem being extensively discussed con fuoco in the Vim issue tracker.\nThis problem is actually documented. On the Vim side, it\u0026rsquo;s in the help, without any mention of how to fix it.\nOn the kitty side, the FAQ mentions it, and also supplies some stuff to put in your .vimrc to make Vim happy. I have not personally tried any of these, and don\u0026rsquo;t understand a single thing about what they\u0026rsquo;re doing. Just for your reference.\nI just want it work. Right now. Try TERM=xterm-256color vim /my/file.txt.\nIt seems like by setting TERM to xterm-256color, Vim will understand at least the paste part mostly fine. I don\u0026rsquo;t know if bracketed paste is happening. This is highly discouraged per kitty\u0026rsquo;s documentation and opinion. Don\u0026rsquo;t use it long term, or you\u0026rsquo;ll find other inexplicable weirdness (which I had, but it\u0026rsquo;s all a blurry mess, so I can\u0026rsquo;t recite to you the war story, alas).\nOther solutions As far as I know, all other terminals pretend to be xterm well enough that Vim plays happily long. So you use them instead. I will keep a copy of Konsole on hand for situations where I have to use vim.\nNeovim doesn\u0026rsquo;t seem to have any issue with kitty\u0026rsquo;s terminfo.\nI prefer Neovim, and got in a habit to go out of my way to replace the default Vim on every system, in part because of this paste weirdness. Recently I got lazy and just went with the bundled Vim. This issue resurfaced, and I finally decided to research it in depth. I couldn\u0026rsquo;t find any internet echos regarding this issue apart from the ones I linked here. Maybe my searching skills have deteriorated. Maybe search engines are not as good anymore. In any case, hopefully writing this adds another data point.\nhappened on a local ArchLinux machine\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhappened on SSH to a Debian Bookworm arm64 supplied by AWS EC2\u0026#160;\u0026#x21a9;\u0026#xfe0e;\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://www.rtk0c.com/blog/vim-kitty-compat/","summary":"\u003cp\u003e\u003cstrong\u003eTL;DR\u003c/strong\u003e: kitty uses a custom terminfo \u003ccode\u003exterm-kitty\u003c/code\u003e. Vim doesn\u0026rsquo;t like it.\nIf you\u0026rsquo;re in a pinch, commit a \u003ca href=\"/blog/vim-kitty-compat/#i-just-want-it-work-right-now\"\u003ecrime\u003c/a\u003e, and hopefully it works fine.\nIf you\u0026rsquo;re not, switch to another terminal for vim, or switch to neovim, or \u003ca href=\"/blog/vim-kitty-compat/#configure-vim\"\u003eattempt to teach vim to speak kitty\u003c/a\u003e.\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003eIf you use Vim in kitty, local machine or going through SSH, and (at least) one of these is happening:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003ePaste \u003ckbd\u003eCtrl+Shift+V\u003c/kbd\u003e from system clipboard is egregiously slow. Like two lines per second slow\u003csup id=\"fnref:1\"\u003e\u003ca href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e1\u003c/a\u003e\u003c/sup\u003e.\u003c/li\u003e\n\u003cli\u003ePaste is glitchy. All the whitespace get eaten, nowhere to be seen. Lines get jumbled together, parts of the clipboard overwrite another, etc\u003csup id=\"fnref:2\"\u003e\u003ca href=\"#fn:2\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e2\u003c/a\u003e\u003c/sup\u003e.\u003c/li\u003e\n\u003cli\u003ekitty tells you your clipboard contains terminal escape sequences. Except it absolutely does not. Pasting elsewhere, still in kitty, like into \u003ccode\u003ebash\u003c/code\u003e or \u003ccode\u003envim\u003c/code\u003e works completely fine\u003csup id=\"fnref1:2\"\u003e\u003ca href=\"#fn:2\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e2\u003c/a\u003e\u003c/sup\u003e.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003ethen congratulations, you have just discovered that Vim isn\u0026rsquo;t very compatible with kitty as a terminal emulator. Instead of trying to poorly summarize \u003cem\u003ewhy\u003c/em\u003e, you can instead read the problem being extensively discussed \u003cem\u003econ fuoco\u003c/em\u003e in the \u003ca href=\"https://github.com/vim/vim/issues/11729\"\u003eVim issue tracker\u003c/a\u003e.\u003c/p\u003e","title":"Vim really does not like kitty"},{"content":"A story of frantically rescuing a deployed headless server, where I forget to statically assign an IP address. It just won\u0026rsquo;t have network connection. No SSH. No fixie.\nExcept there is nothing saying a DHCP server has to be running on the router or gateway. So, just install and configure kea (or a DHCP server of your choice) on any other computer on the network, reboot the server in question, and voilà! SSH to your hearts content.\nSo the story goes like this. I recently got hands on a nice little old desktop tower, plenty of RAM and a good number of SATA ports for self-hosting: Seafile, Immich and what not. Now it is rather old machine, with only VGA and DVI on the motherboard. This means my little trusty HDMI to USB video capture dongle won\u0026rsquo;t be helpful! I also need to deploy this machine to my parent\u0026rsquo;s house, for I plan on giving them access to a photo backup solution. I need to bring a monitor that has a VGA port on it.\nSo I at the comfort of my home, I took some time to install Debian, as well as all other packages I could possibly need. Also configured a SSH key.\nAnd as all good stories go, the one thing I was supposed to do was not done. I didn\u0026rsquo;t assign a static IP to eth0 beforehand.\nUh. oh.\nThere is no proper DHCP in the LAN, I had everything else setup with static IPs. I don\u0026rsquo;t know its MAC, so no calculating SLAAC by hand to get a link local IPv6 either. In fact, if I remember correctly I don\u0026rsquo;t even think the gateway was properly setup with IPv6 at all.\nNo network, no SSH.\nNo monitor, no way to see what\u0026rsquo;s happening on screen to configure its WiFi connection properly.\nQuickly, I thought \u0026ldquo;what if I could just type out all the commands without a monitor?\u0026rdquo; Trying to open vim on /etc/network/interfaces and blindly modifying a complex configuration file obviously did not work so well. Although I did figure out one helpful tidbit: since this machine has a beeper, I can run things like foobar \u0026amp;\u0026amp; tput bel in TTY to get an audio confirmation that something succeeded.\nAfter ten minutes of desperately trying various commands and questioning if I had been making typos all along, an enlightenment suddenly found its way into my mind: nothing is stopping me from running an ad-hoc DHCP server just for this purpose! As far as I know, all the networking implementations shipped by various Linux distros default to DHCP. This includes NetworkManager, systemd-networkd, or even Debian\u0026rsquo;s default networking setup.\nKea is apparently the recommended implementation, so I installed it on my laptop, and after some fiddling of configs per the ArchWiki (because ArchWiki is the one wiki to rule them all), it did work. The server allocates the first address in the pool, so I just picked 192.168.233.1/16 and successfully SSH-ed in.\nNo need to drive 30 minutes round trip to get my VGA monitor!\n","permalink":"https://www.rtk0c.com/blog/adventures-on-monitorless-server/","summary":"\u003cp\u003eA story of frantically rescuing a deployed headless server, where I forget to statically assign an IP address.\nIt just won\u0026rsquo;t have network connection. No SSH. No fixie.\u003c/p\u003e\n\u003cp\u003eExcept \u003cem\u003ethere is nothing saying a DHCP server has to be running on the router or gateway\u003c/em\u003e.\nSo, just install and configure \u003ccode\u003ekea\u003c/code\u003e (or a DHCP server of your choice) on any other computer on the network, reboot the server in question, and voilà!\nSSH to your hearts content.\u003c/p\u003e","title":"Adventures on server setup without a monitor"},{"content":"Note this intended for relative networking novices, so I will try to explain every term used. Skip over them if you find it verbose. If you don\u0026rsquo;t care about anything else and just wants to replicate my setup for your home server, go to this section. Read the TL;DR\u0026rsquo;s in there if that section alone is too long for you too.\nMotivation Virtual mesh networking software, like Tailscale, ZeroTier, tinc, Hamachi and else, practically1 cannot establish a direct/p2p connection between a machine on the SJSU wifi and a machine somewhere else, running on a common residential internet. This situation is an example of a hard-NAT to easy-NAT connection (I\u0026rsquo;m using terminology from Tailscale\u0026rsquo;s article on NAT traversal). I really only use Tailscale, so that\u0026rsquo;s what I\u0026rsquo;m concerned with here.\nTailscale has an excellent relay service that can guarantee a connection between two machines even if it can\u0026rsquo;t establish a direct connection. It has surprisingly good latency, mostly under 50ms for me going from SJSU wifi to a home server. But it has really limited bandwidth, on average 15Mbps based on a quick iperf3 benchmark; this translates to about 1.2MB/s file transfer to my home server (from my experience), which isn\u0026rsquo;t satisfactory for every task.\nSome Background SJSU\u0026rsquo;s network infrastructure works as follows (as of writing this, 2024-05-01):\nThere are 2 wifi, SJSU_Premier and SJSU_Guest available to students and faculty. The subnet is 10.0.0.0/8. This means, for our purposes, every machine connected to the wifi will get a Local-Area Network/LAN (\u0026ldquo;the wifi\u0026rdquo;) IP address between 10.0.0.1 to 10.255.255.254. A subnet is, for our purposes, just a range of IP address that all machines connected in a LAN will get their local IP address from. Both of them seem to be on the same subnet, i.e. machine A in SJSU_Guest can reach machine B in SJSU_Premier directly. This is based on my testing that joining to either one seems to allow connecting to another machine on the VPN. The gateway of the network is an endpoint-dependent firewall and endpoint-dependent NAT (this combination is what \u0026ldquo;hard NAT\u0026rdquo; describes). I assume this is some enterprise grade equipment from Cisco, though that\u0026rsquo;s not super relevant. No IPv6 support whatsoever, both when connecting to the internet and inside the LAN. SJSU also provides a VPN service based on the Cisco AnyConnect software. It is designed to be used for two purposes. First, like a traditional VPN: once you setup the client, all traffic is routed to become originated from SJSU\u0026rsquo;s network; presumably to make some pay-walled text lending service available to who need it at home. Second, it allows you to reach any machine on the subnet 10.0.0.0/8 (both wifi\u0026rsquo;s, as said above), in order to allow faculty to connect to services hosted only on the LAN2.\nI wanted to utilize the second feature, to make Tailscale connect to my home server over \u0026ldquo;LAN\u0026rdquo; created by the VPN. For example if my home server had IP 10.0.12.1 from the VPN, my laptop will be able to connect by that IP directly. Tailscale will pick this up, avoiding having go through their relay.\nMy home server is running linux. You can very much accomplish the same thing on Windows or macOS since Cisco provides VPN software for those too. You also won\u0026rsquo;t need to jump through the hoops I did for linux.\nMy Journey What I need to do is basically two things. (1) Setup Cisco AnyConnect on my home server. (2) Make it so that only the LAN subnet goes through the VPN, not all internet traffic. (I don\u0026rsquo;t need to pretend, for example github.com, to be coming from SJSU\u0026rsquo;s network). Number (2) is technically optional but a nice to have.\nSetting up the VPN TL;DR: I used openconnect-sso on my browser to generate the VPN session token, and copy that to my home server over ssh, and launch OpenConnect with said token. This is because SJSU\u0026rsquo;s account needs to authenticate with Okta/Duo, and that needs a browser.\nI can either use Cisco\u0026rsquo;s official linux software, or use a 3rd-party, open source reimplementation like OpenConnect. I strongly preferred the latter since Cisco\u0026rsquo;s official software wants me to download a blob of bash script to do installation, in addition to downloading another \u0026ldquo;Cisco Secure Desktop\u0026rdquo; executable from the internet, and running it locally on running.\nInstall openconnect-sso using your method of choice. I got it from https://aur.archlinux.org/packages/openconnect-sso\nRun openconnect-sso --server vpn.sjsu.edu --authgroup Student-SSO --user YOUR_SJSU_ID --authenticate\nReplace YOUR_SJSU_ID with, well, your SJSU ID (the 7-digit number)\nThe flag --authenticate tells it to only generate the session token, don\u0026rsquo;t try to create a tunnel.\nThis should print out something like\nHOST=https://vpn.sjsu.edu/ COOKIE=\u0026lt;a very long hexdecimal string\u0026gt; FINGERPRINT=\u0026lt;a slightly shorter hexdecimal string\u0026gt; From what I understood, COOKIE is Cisco AnyConnect\u0026rsquo;s session token, which is only usable once. (That is to say, once you\u0026rsquo;ve connected to the VPN once with the step below, you need to do this current step again to get a new COOKIE.)\nThen, go to your machine that you actually wish the VPN to run on. In my case, it\u0026rsquo;s my personal server ssh rtk0c@my-priv-server\nRun, in place of the ... copy paste the tokens you got from the last step\n$ export HOST=... $ export COOKIE=... $ export FINGERPRINT=... $ echo $COOKIE | sudo openconnect $HOST --cookie-on-stdin --servercert $FINGERPRINT You will leave the openconnect process running, since it is the VPN connection itself. i.e., it pushes wraps internet traffic and pushes them through an encrypted tunnel. Pass the token over stdin to avoid it lingering in the command line. Although it\u0026rsquo;s not like it matters, since I control the whole server.\nTest with curl http://icanhazip.com, it should return an IP that belongs to SJSU. I got 130.65.9.242.\nUn-route the internet from the VPN TL;DR: use ip route del default dev tun0 to get rid of the routing rule for all traffic, and then use ip route add 10.0.0.0/8 dev tun0 to make the LAN subnet accessible.\nopenconnect automatically sets up a routing rule in the linux kernel that sends all internet traffic (i.e. every non-private-use IP address) and the subnet 10.0.0.0/8 through its tunnel, except those going to IP address of SJSU VPN server.\nA tunnel manifests itself as a network interface in the linux kernel, in this case named tun0, just like a wifi card shows up as a network interface. Routing rules tell the kernel, when you see packets coming from such and such, and going to such and such IP address, send it through this network interface. A private-use IP address is one reserved by the IP standard, such that it will never appear on the internet. They\u0026rsquo;re only used inside a LAN.\nI want to get rid of the routing rules for all internet traffic. You can list routing rules with ip route3, in which you should see something like:\ndefault via 10.40.25.168 dev tun0 default via 192.168.1.1 dev wlp1s0 proto dhcp src 192.168.1.142 metric 600 10.40.16.0/20 dev tun0 scope link 130.65.9.242 via 192.168.1.1 dev wlp1s0 src 192.168.1.142 metric 600 130.65.9.242 via 192.168.1.1 dev wlp1s0 src 192.168.1.142 metric 20600 ... rest are omitted ... Each line here is a routing rule. They rules take priority not by their order, but by how specific they are. They more specific (longer the subnet prefix), the higher priority it has.\nSubnet prefix length is the number of bits in the subnet mask. For example, 10.0.0.0/8\u0026rsquo;s prefix is length is 8, so it\u0026rsquo;s less specific than 10.40.16.0/20, which has 20 bits. See your favorite search engine for more if you\u0026rsquo;re curious—the details don\u0026rsquo;t matter here.\nThe first line, default via 10.40.25.168 dev tun0, means that if the destination IP address doesn\u0026rsquo;t match anything below (\u0026ldquo;default\u0026rdquo;), send it to the device tun0 (\u0026ldquo;dev tun0\u0026rdquo;). The 2nd line is the normal rule for my local wifi connection (internet traffic goes to the router). The 3rd, 4th, and 5th lines all come from OpenConnect. 3rd says if the destination IP is in the 10.40.16.0/20 subnet, send it over tun0; even if this rule didn\u0026rsquo;t exist, packets going to the whole SJSU LAN subnet will be caught by the first rule, so it\u0026rsquo;s unnecessarycitation needed. 4th says if the destination IP is exactly 130.65.8.242, which is SJSU\u0026rsquo;s VPN sever, send it over my actual wifi interface (\u0026ldquo;dev wlp1s0\u0026rdquo;); 5th is a duplicate but with a higher metric. I\u0026rsquo;m not sure why it writes these rules with so much redundancy.\nMetric is a number indicating the cost of a route. The higher this number, the less likely the kernel will consider it if other options exist.\nIn any case- all we need to do is get rid of the first line, and then add another rule to cover the whole 10.0.0.0/8 subnet (the current 3rd rule only covers a small section of the subnet). So we\u0026rsquo;ll run:\n$ sudo ip route del default dev tun0 $ sudo ip route add 10.0.0.0/8 dev tun0 Now test with curl http://icanhazip.com again. I got my normal, home IP address back! And test if SJSU\u0026rsquo;s LAN subnet is reachable with ping 10.0.0.1. (I need a machine on the SJSU network, typically the \u0026hellip;1 machine is used by the router, I tried it, and indeed it exists—though I\u0026rsquo;m not sure what it is, but existence is all that matters).\nScript I wrote a bash script sjsu.vpn.sh, to update the token I just copy paste them to the top of the file, as variables.\n#! /bin/bash HOST=https://vpn.sjsu.edu/ COOKIE=000 #your token FINGERPRINT=000 #your fingerprint # https://stackoverflow.com/a/1885534 read -p \u0026#34;Replacing the default everything route with 10.0.0.0/8 only route? (y/N)\u0026#34; REPLY echo #Move to next line if [[ $REPLY =~ ^[Yy]$ ]] then ROUTE_LAN_ONLY=true fi echo $COOKIE | sudo openconnect $HOST --cookie-on-stdin --servercert $FINGERPRINT \u0026amp; if [[ $ROUTE_LAN_ONLY = true ]] then sudo ip route del default dev tun0 \u0026amp;\u0026amp; sudo ip route add 10.0.0.0/8 dev tun0 fi onexit() { kill $(jobs -p) if [[ $ROUTE_LAN_ONLY = true ]] then sudo ip route del 10.0.0.0/8 dev tun0 fi } trap \u0026#39;onexit\u0026#39; EXIT wait Results iperf (and iperf3) speed went from ~15Mbps on tailscale relay to ~55Mbps over the Cisco VPN; ping didn\u0026rsquo;t change meaningfully.\nClosing Thoughts I\u0026rsquo;m not sure if SJSU\u0026rsquo;s Cisco AnyConnect service is going through another hop on a relay server of their own, or it\u0026rsquo;s just a direct connection. I was more or less expecting the latency to be better than going through Tailscale\u0026rsquo;s relay in SFO, though it is what it is.\nI use ZeroTier for setting up game servers with my friends (advantage over Tailscale: no need for signing up an account). ZT doesn\u0026rsquo;t want to listen on the 10.xxx.yyy.zzz address associated with the VPN, so even with the VPN in place, it still uses its own relay. I have no idea why, it could be its discovery mechanism (UDP local broadcast) is blocked by SJSU\u0026rsquo;s network, or there is some kind of internal blacklist mechanism for blocking the tun0 device used by OpenConnect. A quick github search in their source yield too many results for me to dig through; google did not hint at anything relevant.\nSome software like Tailscale have some heuristics to more-or-less brute force a direction connection between hard-NAT and easy-NAT. It takes quite a bit of luck for this to happen in my experience: for the close to 1 year I\u0026rsquo;ve been here, direction connection only ever happened once.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n\u0026ldquo;VPN allows users outside the SJSU network access to restricted resources (like file shares, servers, and desktops) on the SJSU network, as if they are physically located on the SJSU campus network behind secured firewalls.\u0026rdquo; https://sjsu.edu/it/services/network/internet-access/vpn.php\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nLinux has the concept of different routing tables. ip route only shows the main routing table, but that\u0026rsquo;s all we care about here. You can use ip route show table \u0026lt;table name\u0026gt; to show a specific table. Tailscale routes packets to the tailnet IP addresses (the ones like 100.xxx.xxx.xxx) in the routing table 52.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://www.rtk0c.com/blog/tailscale-and-sjsu-vpn/","summary":"\u003cp\u003eNote this intended for relative networking novices, so I will try to explain every term used. Skip over them if you find it verbose. If you don\u0026rsquo;t care about anything else and just wants to replicate my setup for your home server, go to \u003ca href=\"/blog/tailscale-and-sjsu-vpn/#my-journey\"\u003ethis section\u003c/a\u003e. Read the TL;DR\u0026rsquo;s in there if that section alone is too long for you too.\u003c/p\u003e\n\u003ch1 id=\"motivation\"\u003eMotivation\u003c/h1\u003e\n\u003cp\u003eVirtual mesh networking software, like Tailscale, ZeroTier, tinc, Hamachi and else, practically\u003csup id=\"fnref:1\"\u003e\u003ca href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e1\u003c/a\u003e\u003c/sup\u003e cannot establish a direct/p2p connection between a machine on the SJSU wifi and a machine somewhere else, running on a common residential internet. This situation is an example of a hard-NAT to easy-NAT connection (I\u0026rsquo;m using terminology from \u003ca href=\"https://tailscale.com/blog/how-nat-traversal-works\"\u003eTailscale\u0026rsquo;s article on NAT traversal\u003c/a\u003e). I really only use Tailscale, so that\u0026rsquo;s what I\u0026rsquo;m concerned with here.\u003c/p\u003e","title":"Setting up SJSU VPN for connection to home server"},{"content":"2025 2025-10 October 2025-10-06 Boot black screen fix CSS snippet for LLM chat\n","permalink":"https://www.rtk0c.com/llm-log/","summary":"\u003ch1 id=\"2025\"\u003e2025\u003c/h1\u003e\n\u003ch2 id=\"2025-10-october\"\u003e2025-10 October\u003c/h2\u003e\n\u003ch3 id=\"2025-10-06\"\u003e2025-10-06\u003c/h3\u003e\n\u003cp\u003e\u003ca href=\"https://paste.rtk0c.com/uzfJ8CE2.html\"\u003eBoot black screen fix\u003c/a\u003e \u003cbr\u003e\n\u003ca href=\"https://paste.rtk0c.com/1ZcXVSGd.html\"\u003eCSS snippet for LLM chat\u003c/a\u003e\u003c/p\u003e","title":"LLM Log Exports"}]