<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>my tech blog &#187; Internet</title>
	<atom:link href="http://billauer.se/blog/category/internet/feed/" rel="self" type="application/rss+xml" />
	<link>https://billauer.se/blog</link>
	<description>Anything I found worthy to write down.</description>
	<lastBuildDate>Thu, 12 Mar 2026 11:36:00 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.1.2</generator>
		<item>
		<title>Perl one-liner for adding newlines to HTML</title>
		<link>https://billauer.se/blog/2026/03/perl-tidy-html/</link>
		<comments>https://billauer.se/blog/2026/03/perl-tidy-html/#comments</comments>
		<pubDate>Thu, 12 Mar 2026 11:35:32 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Internet]]></category>
		<category><![CDATA[perl]]></category>
		<category><![CDATA[Rich text editors]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=7235</guid>
		<description><![CDATA[When the rich editor puts all HTML in one line, and I want to edit it, I could always use the &#8220;tidy&#8221; utility, however it does too much. All I want is a newline here and there to make the whole thing accessible. So this simple one-liner does the job: perl -pe 's/(&#60;\/(?:p&#124;h\d&#124;div&#124;tr&#124;td&#124;table&#124;ul&#124;ol&#124;li)&#62;)/"$1\n"/ge' Not perfect, [...]]]></description>
			<content:encoded><![CDATA[<p>When the rich editor puts all HTML in one line, and I want to edit it, I could always use the &#8220;tidy&#8221; utility, however it does too much. All I want is a newline here and there to make the whole thing accessible.</p>
<p>So this simple one-liner does the job:</p>
<pre>perl -pe 's/(&lt;\/(?:p|h\d|div|tr|td|table|ul|ol|li)&gt;)/"$1\n"/ge'</pre>
<p>Not perfect, but gives something to work with.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2026/03/perl-tidy-html/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Altering the Message-ID header in Thunderbird for non-spam detection</title>
		<link>https://billauer.se/blog/2024/08/mail-message-id-spam-filtering/</link>
		<comments>https://billauer.se/blog/2024/08/mail-message-id-spam-filtering/#comments</comments>
		<pubDate>Sat, 10 Aug 2024 10:38:18 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[email]]></category>
		<category><![CDATA[Internet]]></category>
		<category><![CDATA[Server admin]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=7125</guid>
		<description><![CDATA[TL;DR In this post, I suggest manipulating the Message IDs of outgoing mails, so that legit inbound replies to my mails are easily detected as non-spam. I also show how to do this with Thunderbird (Linux version 91.10.0, but it works with practically all versions, I believe). Briefly about Message-ID Each email should have a [...]]]></description>
			<content:encoded><![CDATA[<h3>TL;DR</h3>
<p>In this post, I suggest manipulating the Message IDs of outgoing mails, so that legit inbound replies to my mails are easily detected as non-spam. I also show how to do this with Thunderbird (Linux version 91.10.0, but it works with practically all versions, I believe).</p>
<h3>Briefly about Message-ID</h3>
<p>Each email should have a Message-ID header, which uniquely identifies this message. The value of this header <a rel="noopener" href="https://www.rfc-editor.org/rfc/rfc2822#section-3.6.4" target="_blank">should consist</a> of a random string, followed by an &#8216;@&#8217; and a string that represents the domain name (referred to as FQDN, Fully Qualified Domain Name). This is often the full domain name of the &#8220;From&#8221; header (e.g. gmail.com).</p>
<p>For example, an email generated by Gmail&#8217;s web client had Message-ID: &lt;CAD8P7-R2OuJvGiuQ-0RQqgSSmDguwv1VdjHgQND4jMJxPc628w@mail.gmail.com&gt;. A similar result (same FQDN) was obtained when sending from the phone. However, when using Thunderbird to send an email, only &#8220;gmail.com&#8221; was set as the FQDN.</p>
<h3>Does the Message-ID matter?</h3>
<p>Like anything related to email, there are a lot of actors, and each has its own quirks. For example, rspamd adds the spam score by 0.5, with the MID_RHS_NOT_FQDN rule, if the Message ID isn&#8217;t an FQDN. I&#8217;m not sure to which extent it checks that the FQDN matches the email&#8217;s From, but even if it does, it can&#8217;t be that picky, given the example I showed above in relation to gmail.com.</p>
<p>It&#8217;s quite rare that people care about this header. I&#8217;ve <a rel="noopener" href="https://forum.emclient.com/t/message-id-contains-local-computer-name-can-it-be-changed/68828" target="_blank">seen somewhere</a> that someone sending mails from a work computer didn&#8217;t like that the name of the internal domain leaking.</p>
<p>All in all, it&#8217;s probably a good idea to make sure that the Message-ID header looks legit. Putting the domain from the From header seems to be a good idea to keep spam filters happy.</p>
<h3>Why manipulate the Message-ID?</h3>
<p>In an reply, the In-Reply-To header gets the value of the Message ID of the message replied to. So if a spam filter can identify that that the email is genuinely a reply to something I sent, it&#8217;s definitely not spam. It&#8217;s also a good idea to scan the References header too, in order to cover more elaborate scenarios when there are several people corresponding.</p>
<p>The rigorous way to implement this spam filtering feature is storing the Message IDs of all sent mails in some small database, and check for a match with the content of In-Reply-To of arriving mails. Possible, however daunting.</p>
<p>A much easier way is to change the FQDN part, so that it&#8217;s easily identifiable. This is unnecessary if you happen send emails with your own domain, as spam senders are very unlikely to add an In-Reply-To with a matching domain (actually, very few spam messages have an In-Reply-To header at all).</p>
<p>But for email sent through gmail, changing the FQDN to something unique is required to make a distinction.</p>
<p>Will this mess up things? I&#8217;m not sure any software tries to fully match the FQDN with the sender, but I suppose it&#8217;s safe to add a subdomain to the correct domain. I mean, if both &#8220;mail.gmail.com&#8221; and &#8220;gmail.com&#8221; are commonly out there, why shouldn&#8217;t &#8220;secretsauce.gmail.com&#8221; seem likewise legit to any spam filter that checks the message?</p>
<p>And by the way, as of August 2024, a DNS query for mail.gmail.com yields no address, neither for A nor MX. In other words, Gmail itself uses an invalid domain in its Message ID, so any other invented subdomain should do as well.</p>
<h3>Changing the FQDN on Thunderbird</h3>
<p>Click the hamburger icon, choose Preferences, and scroll down all the way (on the General tab) and click on Config Editor.</p>
<p>First, we need to find Thunderbird&#8217;s internal ID number for the mail account to manipulate.</p>
<p>To get a list of IDs, write &#8220;useremail&#8221; in the search text box. This lists entries like mail.identity.id1.useremail and their values. This listing allows making the connection between e.g. &#8220;id1&#8243; and the email address related to it.</p>
<p>For example, to change the FQDN of the mail account corresponding to &#8220;id3&#8243;, add a string property (using the Config Editor). The key of this property is &#8220;mail.identity.id3.FQDN&#8221; and the value is something like &#8220;secretsauce.gmail.com&#8221;.</p>
<p>There is no need to restart Thunderbird. The change is in effect on the next mail sent, and it remains in the settings across restarts.</p>
<p>The need for this feature has been questioned, as was discussed <a rel="noopener" href="https://thunderbird.topicbox.com/groups/planning/T53e9959c6d6c12a9-M030a61c790caab2d7763a25b" target="_blank">here</a>. So if any Thunderbird maintainer reads this, please keep this feature up and running.</p>
<h3>A possible alternative approach</h3>
<p>Instead of playing around with the Message-ID, it would be possible to add an entry to the References header (or add this header if there is none). The advantage of this way is that this can also be done by the MTA further down the delivery path, and it doesn&#8217;t alter anything that is already in place.</p>
<p>And since it&#8217;s an added entry, it can also be crafted arbitrarily. For example, it may contain a timestamp (epoch time in hex) and the SHA1 sum of a string that is composed by this timestamp and a secret string. This way, this proof of genuine correspondence is impossible to forge and may expire with time.</p>
<p>I haven&#8217;t looked into how to implement this in Thunderbird. Right now I&#8217;m good with the Message-ID solution.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2024/08/mail-message-id-spam-filtering/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>ssl.com stealing from my credit card, again</title>
		<link>https://billauer.se/blog/2024/05/ssl-com-renewal-fraud/</link>
		<comments>https://billauer.se/blog/2024/05/ssl-com-renewal-fraud/#comments</comments>
		<pubDate>Mon, 13 May 2024 04:16:04 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[crypto]]></category>
		<category><![CDATA[Internet]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=7064</guid>
		<description><![CDATA[Credit card abuse, episode #2 ssl.com presents the lowest price for an EV code signing certificate, however it&#8217;s a bit like going into a flea market with a lot of pickpockets around: Pay attention to your wallet, or things happen. This is a follow-up post to one that I wrote three years ago, after ssl.com [...]]]></description>
			<content:encoded><![CDATA[<h3>Credit card abuse, episode #2</h3>
<p>ssl.com presents the lowest price for an EV code signing certificate, however it&#8217;s a bit like going into a flea market with a lot of pickpockets around: Pay attention to your wallet, or things happen.</p>
<p>This is a follow-up post to <a rel="noopener" href="https://billauer.se/blog/2021/11/esigner-cloud-signing-ssl-com-certificate/" target="_blank">one that I wrote three years ago</a>, after ssl.com suddenly charged my credit card in relation to the eSigner service. It turns out that this a working pattern that persists even three years later. Actually, it was $200 last time, and $747 now, so one could say they&#8217;re improving.</p>
<h3>The autorenew fraud</h3>
<p>Three years ago, I got a EV code signing certificate from ssl.com that expired more or less at the time of writing this. I got a reminder email from ssl.com, urging me to renew this certificate, and indeed, I ordered a one-year certificate so I could continue to sign drivers. I paid for the one-year certificate and went through a brief process of authenticating my identity, and got an approval soon enough.</p>
<p>I&#8217;ll say a few words below about the technicalities around getting the certificate, but all in all the process was finished after a few days, and I thought that was the end of it.</p>
<p>And then, I randomly checked my credit card bill and noticed that it had been charged with 747 USD by ssl.com. So I contacted them to figure out what happened. The answer I got was:</p>
<blockquote><p><span class="yadayada">{order number}</span> is an auto renewal for the expiring order. But, I do see that you already manually renewed and renewal cert issued.</p>
<p>I can cancel <span class="yadayada">{order number}</span> then credit the amount to your SSL.com account. Would that be good with you?</p></blockquote>
<p>Indeed, the automatic renewal order was issued after I had completed the process with the new certificate, so surely there was no excuse for an automatic renewal. And the offer to add the funds to my account in ssl.com for future use was of course a joke (even though they were serious about it, of course).</p>
<p>It&#8217;s worth mentioning that the reminder email said nothing about what would happen if I didn&#8217;t renew the certificate. And surely, there was no hint about any automatic mechanism for a renewal.</p>
<p>On top of that, I got no notification whatsoever about the automatic renewal or that my credit card had been charged. Needless to say, I didn&#8217;t approve this renewal. In fact, I made the order for the one-year certificate on a different and temporary credit card, because I learned the lesson from three years ago. Or so I thought.</p>
<p>So I asked them to cancel the order and refund my credit card. Basically, the answer I got was</p>
<blockquote><p>I have forwarded to the billing team about the refund request. They will email you once they have an update.</p></blockquote>
<p>Sounds like a fairly happy end, doesn&#8217;t it? Only they didn&#8217;t cancel the order, let alone refund the credit card. During two weeks I sent three reminders, and the answer was repeatedly that my requests and reminders had been forwarded to &#8220;the team&#8221;, and that&#8217;s where it ended. Who knows, maybe I&#8217;ll just forget about it.</p>
<p>I sent the fourth reminder to billing@ssl.com (and not support@ssl.com), so I got some kind of response. I was once again offered to fill up my wallet on ssl.com with the money instead of a refund. To which I responded negatively, of course. In fact, I turned to slightly harsher language, saying that ssl.com&#8217;s conduct makes them no better than a street pickpocket.</p>
<p>And interestingly enough, the response was that my refund request &#8220;had been approved&#8221;. A day later, I got a confirmation that a refund had taken place. The relevant order remained in the ssl.com&#8217;s Dashboard as &#8220;pending validation&#8221;, but at the same time also marked as refunded. And indeed, the refund was visible in my credit card bill the day after that.</p>
<p>So the method is to fetch money silently from the credit card, hoping that I won&#8217;t pay attention or won&#8217;t bother to do anything about it. Is there another definition for stealing? And I guess this method works fine with companies that have a lot of transactions of this sort with their credit cards. A few hundred dollars can easily slip away.</p>
<p>It appears like the counter-tactic is to use angry and ugly language right away. As long as the request for refund is polite and sounds like a calm person has written it, there&#8217;s still hope that the person writing it will give up or maybe forget about it.</p>
<p>And by the way, this post was published after receiving the refund, so unlike last time, it didn&#8217;t play a role in getting the issue resolved.</p>
<h3>Avoiding unexpected withdrawals</h3>
<p>The best way to avoid situations like this is of course to use a credit card with a short life span. This is the kind I used this time, but not three years ago.</p>
<p>Specifically with ssl.com, there are two things to do when ordering a certificate from them:</p>
<ul>
<li>After purchasing, be sure that autorenewal is off. Click the &#8220;settings&#8221; link on the Dashboard, and uncheck &#8220;Automated Certificate Renewal&#8221;.</li>
<li>Also, delete the credit card details: Click on &#8220;deposit funds&#8221; on the Dashboard, and delete the credit card details.</li>
</ul>
<h3>Pushing eSigner, again</h3>
<p>And now to a more subtle issue.</p>
<p>The approval for my one-year certificate came quickly enough, and it came with two suggestions for continuing: To start off immediately with eSigner, or to order a Yubikey from them with the certificate already loaded on it. The latter option costs $279 (even though it was included for free three years ago). Makes the eSigner option sound attractive, doesn&#8217;t it?</p>
<p>They didn&#8217;t mention using the Yubikey dongle that I already had and that I used for signing drivers. It was only when I asked about this option that they responded that there&#8217;s <a rel="noopener" href="https://www.ssl.com/how-to/key-generation-and-attestation-with-yubikey/" target="_blank">a procedure</a> for loading a new certificate into the existing dongle.</p>
<p>And so I did, and filled the automatic form on their website, as required for obtaining a Yubikey-based certificate. And waited. And waited. Nothing happened. So I sent a reminder, got apologies for the delay, and finally got the certificate I had ordered.</p>
<p>Was this an innocent mishap, or a deliberate attempt to make me try out eSigner instead? As I&#8217;ve already <a rel="noopener" href="https://billauer.se/blog/2021/11/esigner-cloud-signing-ssl-com-certificate/" target="_blank">had my fingers burnt with eSigner</a>, no chance I would do that, but I can definitely imagine people losing their patience.</p>
<h3>The Yubikey dongle costs $25</h3>
<p>You can get your Yubikey dongle from ssl.com at $279, or buy it directly from Yubico at $25. <a rel="noopener" href="https://www.yubico.com/il/product/security-key-series/security-key-nfc-by-yubico-black/" target="_blank">This is the device</a> that I got from ssl.com three years ago, and which I use with the renewed certificate after completing the <a rel="noopener" href="https://www.ssl.com/how-to/key-generation-and-attestation-with-yubikey/" target="_blank">attestation procedure</a>.</p>
<p>The idea behind this procedure is that the secret key that is used for digital signatures is created inside the dongle, and is never revealed to the computer (or in any other way), so it can&#8217;t be stolen. The dongle generates a certificate (the &#8220;attestation certificate&#8221;) ensuring that the public key and secret key pair was indeed created this way, and is therefore safe. This certificate is signed with Yubico&#8217;s secret key, which is also present inside the dongle.</p>
<p>So the procedure consists of creating the key pair, obtaining the attestation certificate from the dongle and sending it to ssl.com by filling in a web form. They generate a certificate for signing code (or whatever is needed) in response.</p>
<p>So if you&#8217;re about to obtain your first certificate from ssl.com, I suggest checking up the option to purchase the Yubikey separately from Yubico. They have no reason to refuse from a security point of view, because the attestation certificate ensures that the cryptographic key is safe inside the dongle.</p>
<h3>Summary</h3>
<p>Exactly like three years ago, it seems like ssl.com uses fraudulent methods along with dirty tactics to cover up for their relatively low prices. So if you want to work with this company, be sure to keep a close eye on your credit card bill, and be ready for lengthy delays when requesting something that apparently goes against their interests. Plus misleading messages.</p>
<p>Also, be ready for a long exchange of emails with their support and billing department. It&#8217;s probably best to escalate to rude and aggressive language pretty soon, as their support team is probably instructed not to be cooperative as long as the person complaining appears to be calm.</p>
<p>And this comes from a company whose core business is generating trust.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2024/05/ssl-com-renewal-fraud/feed/</wfw:commentRss>
		<slash:comments>10</slash:comments>
		</item>
		<item>
		<title>Notes on ZTE ZXHN F601 GPON ONT</title>
		<link>https://billauer.se/blog/2023/08/zte-f601-gpon-ont/</link>
		<comments>https://billauer.se/blog/2023/08/zte-f601-gpon-ont/#comments</comments>
		<pubDate>Sun, 06 Aug 2023 11:18:07 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Internet]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=6938</guid>
		<description><![CDATA[Introduction These are my notes while setting up ZTE&#8217;s ONT for GPON on a Linux desktop computer. I bought this thing from AliExpress at 20 USD, and got a cartoon box with the ONT itself, a power supply and a LAN cable. This is a follow-up from a previous post of mine. I originally got [...]]]></description>
			<content:encoded><![CDATA[<h3>Introduction</h3>
<p>These are my notes while setting up ZTE&#8217;s ONT for GPON on a Linux desktop computer. I bought <a rel="noopener" href="https://www.aliexpress.com/item/1005005697568614.html" target="_blank">this thing</a> from AliExpress at 20 USD, and got a cartoon box with the ONT itself, a power supply and a LAN cable.</p>
<p>This is a follow-up from a <a rel="noopener" href="https://billauer.se/blog/2023/07/fiber-internet-israel-gpon/" target="_blank">previous post of mine</a>. I originally got a Nokia ONT when the fiber was installed, but I wanted an ONT that I can talk with. In particular, one that gives some info about the fiber link. Just in case something happens.</p>
<p>The cable of the 12V/0.5A power supply was too short for me, so I remained with the previous one (from Nokia&#8217;s ONT).</p>
<p>The software version of the ONT is V6.0.1P1T12 out of the box, which is certified by Bezeq. Couldn&#8217;t be better.</p>
<p>By default, this ONT acts as a GPON to Ethernet bridge. However, judging by its menus on the browser interface, it can also act as a router with one Ethernet port: If so requested, it apparently takes care of the PPPoE connection by itself, and is capable of supplying the whole package that comes with a router: NAT, a firewall, a DHCP server, a DNS and whatnot. I didn&#8217;t try any of this, so I don&#8217;t know how well it works. But it&#8217;s worth to keep these possibilities in mind.</p>
<p>In order to reset the ONT&#8217;s settings to the default values, press the RESET button with a needle for at least five seconds while the device is on (according to the user manual, didn&#8217;t try this).</p>
<p>So how come this thing isn&#8217;t sold at ten times the price, rebranded by some big shot company? I think the reason is this:</p>
<h3>The PON LED is horribly misleading</h3>
<p>According to the user guide, the PON LED is off when the registration has failed, blinking when registration is ongoing, and steadily on when registration is successful.</p>
<p>The problem is that registration doesn&#8217;t mean authentication. In other words, the fact that the PON LED is steadily on doesn&#8217;t mean that the other side (the OLT) is ready to start a PPPoE session. In particular, if the PON serial number is not set up correctly, the PON LED will be steadily on, even though the fiber link provider has rejected the connection.</p>
<p>Nokia&#8217;s modem&#8217;s PON led will blink when the serial number is wrong, and it makes sense: The PON is not good to go unless the authentication is successful. I suppose most other ONTs behave this way.</p>
<p>The only way to tell is through the browser interface. More about this below.</p>
<h3>Browser interface</h3>
<p>The ONT responds to pings and http at port 80 on address 192.168.1.1. A Chinese login screen appears. Switch language by clicking on where is says &#8220;English&#8221; at the login box&#8217; upper right corner.</p>
<p>The username and password are both &#8220;admin&#8221; by default.</p>
<p>As already mentioned, this ONT has a lot of features. For me, there were two important ones: The ability to change the PON serial number, so I can replace ONTs without involving my ISP, and the ability to monitor the fiber link&#8217;s status and health. This can be crucial when spotting a problem:</p>
<p><a href="https://billauer.se/blog/wp-content/uploads/2023/08/zte-f601-pon-status-page.png"><img class="aligncenter size-medium wp-image-6939" title="Fiber link information on browser interface of ZTE ZXHN F601 GPON ONT" src="https://billauer.se/blog/wp-content/uploads/2023/08/zte-f601-pon-status-page-300x185.png" alt="Fiber link information on browser interface of ZTE ZXHN F601 GPON ONT" width="300" height="185" /></a></p>
<p style="text-align: center;"><a href="https://billauer.se/blog/wp-content/uploads/2023/08/zte-f601-pon-status-page.png" target="_blank"><em>(click to enlarge)</em></a></p>
<p>Note that in this screenshot, the GPON State is &#8220;Authentication Success&#8221;. This is what it should be. If it says &#8220;Registration Complete&#8221;, it means that the ONT managed through a few stages in the setup process, but the link isn&#8217;t up yet: The other side probably rejected the serial number (and/or the password, if such is used). And by the way, when the fiber wasn&#8217;t connected at all, it said &#8220;Init State&#8221;.</p>
<p>Also note the input power, around -27 dBm in my case. It depends on a lot of factors, among others the physical distance to the other fiber transmitter. It can also change if optical splitters are added or removed on the way. All this is normal. But each such change indicates that something has happened on the optical link. So it&#8217;s a good way to tell if people are fiddling with the optics, for better and for worse.</p>
<p>These are the changes I made on my box, relative to the default:</p>
<ul>
<li>I turned the firewall off at Security &gt; Firewall (was at &#8220;Low&#8221;). It&#8217;s actually possible to define custom rules, most likely based upon iptables. I don&#8217;t think the firewall operates when the ONT functions as a bridge, but just to be sure it won&#8217;t mess up.</li>
<li>In Security &gt; Service Control, there&#8217;s an option for telnet access from WAN. Removed it.</li>
<li>In BPDU, disabled BPDU forwarding.</li>
</ul>
<p>I don&#8217;t think any of these changes make any difference when using the ONT as a bridge.</p>
<h3>Setting the PON serial number</h3>
<p><span style="color: #888888; font-style: italic;">Note to self: Look for a file named pon-serial-numbers.txt for the previous and new PON serial numbers.</span></p>
<p>When I first connected the ONT to the fiber, I was surprised to see that the PON LED flashed and then went steady. Say what? The network accepted the ONT&#8217;s default serial number without asking any questions?</p>
<p>I then looked at the &#8220;PON inform&#8221; status page (Status &gt; Network Interface &gt; PON Inform), and it said &#8220;Registration Complete&#8221;. Wow. That looked really reassuring. However, pppd was less happy with the situation. In fact, it had nobody to talk with:</p>
<pre>Aug 06 10:56:21 pppd[36167]: Plugin rp-pppoe.so loaded.
Aug 06 10:56:21 pppd[36167]: RP-PPPoE plugin version 3.8p compiled against pppd 2.4.5
Aug 06 10:56:21 pppd[36168]: pppd 2.4.5 started by root, uid 0
Aug 06 10:56:56 pppd[36168]: <span class="punch">Timeout waiting for PADO packets</span>
Aug 06 10:56:56 pppd[36168]: <span class="punch">Unable to complete PPPoE Discovery</span>
Aug 06 10:56:56 pppd[36168]: Exit.</pre>
<p>Complete silence from the other side. I was being ignored bluntly.</p>
<p>Note that I&#8217;m discussing the PPPoE topic in <a rel="noopener" href="https://billauer.se/blog/2023/07/linux-kernel-pppoe-pppd/" target="_blank">another post of mine</a>.</p>
<p>Solution: I went into the Network &gt; PON &gt; SN menu in the browser interface, and copied the serial number that was printed on my previous ONT in full. It was something like ALCLf8123456. That is, four capital letters, followed by 8 hex digits. There&#8217;s also a place to fill in the password. Bezeq&#8217;s fiber network apparently doesn&#8217;t use a password, so I just wrote &#8220;none&#8221;. Clicked the &#8220;Submit&#8221; button, the ONT rebooted (it takes about a minute), and after that the Internet connection was up and running.</p>
<p>And of course, the GPON State appeared as &#8220;Authentication Success&#8221; in the &#8220;POD Inform&#8221; page.</p>
<p>So don&#8217;t trust the PON LED, and don&#8217;t get deceived by the words &#8220;Registration Complete&#8221;. Unless you feed the serial number that the fiber network provider expects, there&#8217;s nobody talking with you.</p>
<p>In fact, there&#8217;s an option in browser interface to turn off the LEDs altogether. It seemed like a weird thing to me at first, but maybe this is the Chinese workaround for this issue with the PON LED.</p>
<h3>Bottom line</h3>
<p>With the Internet link up and running, I ran a speed test. Exactly the same as the Nokia ONT.</p>
<p>So the final verdict is that this a really good ONT, which provides a lot of features and information. The only problem it apparently has is the confusing information regarding the PON link&#8217;s status when the serial number is incorrect. Which is probably the reason why this cute thing remains a Chinese no-name product.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2023/08/zte-f601-gpon-ont/feed/</wfw:commentRss>
		<slash:comments>13</slash:comments>
		</item>
		<item>
		<title>http referer info missing in Apache logs for a non-https site</title>
		<link>https://billauer.se/blog/2023/07/http-referrer-missing/</link>
		<comments>https://billauer.se/blog/2023/07/http-referrer-missing/#comments</comments>
		<pubDate>Sun, 30 Jul 2023 07:26:11 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Internet]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=6929</guid>
		<description><![CDATA[I checked my Apache access logs, and noted that I saw no indications for people clicking links between two of my websites. It was extremely odd, because it was quite clear that at least a few such clicks should happen. In the beginning, I though it was because of the rel=&#8221;noopener&#8221; part in the link. [...]]]></description>
			<content:encoded><![CDATA[<p>I checked my Apache access logs, and noted that I saw no indications for people clicking links between two of my websites. It was extremely odd, because it was quite clear that at least a few such clicks should happen.</p>
<p>In the beginning, I though it was because of the rel=&#8221;noopener&#8221; part in the link. It shouldn&#8217;t have anything to do with this, but maybe it did? So no, that wasn&#8217;t the problem.</p>
<p>The issue was that if the link goes from an https site to a non-https site, the referer is blank. Why? Not 100% clear, but this is what <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Referer_header:_privacy_and_security_concerns" target="_blank">Mozilla&#8217;s guidelines</a> says. It has probably to do with pages with sensitive URLs (e.g. pages for resetting passwords). If the URL leaks through a non-secure http link (say, to a third-party server that supplies images, fonts and other stuff for the page), an eavesdropping attacker might get access to this URL.</p>
<p>And it so happens that this blog is a non-https site as of writing this. Mainly because I&#8217;m lazy.</p>
<p>On the other hand, when you read this, the site has been moved to https. Lazy or not, the missing referrer was the motivation I needed to finally do this.</p>
<p>Was it worth the effort? Well, so-so. Both Chrome nor Firefox submit a blank referrer if the link was non-https, even if a redirection to an https address is made. In other words, all existing links to a plain http address will remain hidden. But new links are expected to be based upon https, so at least they will be visible.</p>
<p>Well, partly: My own anecdotal test showed that Firefox indeed submits the full URL of the referrer for an https link, but Chrome gives away only the domain of the linking site. This is more secure of course: Don&#8217;t disclose a sensitive URL to a third party. And also, if you want to know who links to your page, go to Google&#8217;s search console. So chopping off the referrer also server Google to some extent.</p>
<p>Bottom line: It seems like the Referer thing is slowly fading away.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2023/07/http-referrer-missing/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>PPPoE on fiber with the Linux machine as the router</title>
		<link>https://billauer.se/blog/2023/07/linux-kernel-pppoe-pppd/</link>
		<comments>https://billauer.se/blog/2023/07/linux-kernel-pppoe-pppd/#comments</comments>
		<pubDate>Fri, 14 Jul 2023 13:44:05 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Internet]]></category>
		<category><![CDATA[Linux]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=6921</guid>
		<description><![CDATA[Introduction Having switched from ADSL to FTTH (fiber to the home), I was delighted to discover that the same script that set up the pppoe connection for ADSL also works with the new fiber connection. And as the title of this post implies, I opted out the external router that the ISP provided, and instead [...]]]></description>
			<content:encoded><![CDATA[<h3>Introduction</h3>
<p>Having switched from ADSL to FTTH (fiber to the home), I was delighted to discover that the same script that set up the pppoe connection for ADSL also works with the new fiber connection. And as the title of this post implies, I opted out the external router that the ISP provided, and instead my Linux desktop box does the pppoe stuff. Instead, I went for a simple ONT (&#8220;fiber bridge&#8221;) which relays Ethernet packets between the fiber and an Ethernet jack.</p>
<p>This post is a spin-off from <a title="Internet fiber optics in Israel: The gory technical details" href="https://billauer.se/blog/2023/07/fiber-internet-israel-gpon/" target="_blank">another post of mine</a>, which discusses the transition to fiber in general.</p>
<p>Why am I making things difficult, you may ask? Actually, if you&#8217;re reading this there&#8217;s a good chance that you want to do the same, but anyhow: The reason for opting out an external router is the possibility to apply a sniffer on the pppoe negotiation if something goes wrong. To be able to tell the difference between rejected credentials and an ISP that doesn&#8217;t talk with me at all. This might hopefully help bringing back the link quicker if and when.</p>
<p>But then it turned out that even though the old setting works, the performance is quite bad: It was all nice when the data rate was limited to 15 Mb/s, but 1000 Mb/s is a different story.</p>
<p>So here&#8217;s my own little cookbook to pppoe for FTTH on a Linux desktop.</p>
<h3>The &#8220;before&#8221;</h3>
<p>The commands I used for ADSL were:</p>
<pre>/usr/sbin/pppd pty /usr/local/etc/ADSL-pppoe linkname ADSL-$$ user "<span class="yadayada">myusername@013net</span>" remotename "10.0.0.138 RELAY_PPP1" defaultroute netmask 255.0.0.0 mtu 1452 mru 1452 noauth lcp-echo-interval 60 lcp-echo-failure 3 nobsdcomp usepeerdns</pre>
<p>such that /usr/local/etc/ADSL-pppoe reads:</p>
<pre>#!/bin/bash
/usr/sbin/pppoe -p /var/run/pppoe-adsl.pid.pppoe -I eth1 -T 80 -U  -m 1412</pre>
<p>And of course, replace myusername@013net with your own username and assign the password in /etc/ppp/pap-secrets. Hopefully needless to say, the ADSL modem was connected to eth1.</p>
<p>This ran nicely for years with pppd version 2.4.5 and PPPoE Version 3.10, which are both very old. But never mind the versions of the software. pppoe and pppd are so established, that I doubt any significant changes have been made over the last 15 years or so.</p>
<p>Surprisingly enough, I got only 288 Mb/s download and 100 Mb/s upload on <a rel="noopener" href="https://cellcom.co.il/production/Private/Internet/speedtest/" target="_blank">Netvision&#8217;s own speed test</a>. The download speed should have been 1000 Mb/s (and the upload speed is as expected).</p>
<p>I also noted that pppoe ran at 75% CPU during the speed test, which made me suspect that it&#8217;s the bottleneck. Spoiler: It indeed was.</p>
<p>I tried a newer pppd (version 2.4.7) and pppoe (version 3.11) but that made no difference. As one would expect.</p>
<h3>Superfluous options</h3>
<p>Note that pppd gets unnecessary options that set the MTU and MRU to 1452 bytes. I suspected that these were the reason for pppoe working hard, so I tried without them. But there was no difference. They are redundant nevertheless.</p>
<p>Then we have the &#8216;remotename &#8220;10.0.0.138 RELAY_PPP1&#8243; &#8216; part, which I frankly don&#8217;t know why it&#8217;s there. Probably a leftover from the ADSL days.</p>
<p>Another thing is pppoe&#8217;s &#8220;-m 1412&#8243; flag, which causes pppoe to mangle TCP packets with the SYN flag set, so that their MSS option is set to 1412 bytes, and not what was originally requested.</p>
<p>A quick reminder: The MSS option is the maximal size of IP packets that we can receive from the TCP stack on the other side. This option is used to tell the other side not to create TCP packets larger than this, in order to avoid fragmentation of arriving packets.</p>
<p>It is actually a good idea to mangle the MSS on outgoing TCP packets, as explained further below. But the 1412 bytes value is archaic, copied from the pppoe man page or everyone copies from each other. 1452 is a more sensible figure. But it doesn&#8217;t matter all that much, because I&#8217;m about to scrap the pppoe command altogether. Read on.</p>
<h3>Opening the bottleneck</h3>
<p>The solution is simple: Use pppoe in the kernel.</p>
<p>There&#8217;s a whole list of kernel modules that need to be available (or compiled into the kernel), but any sane distribution kernel will have them included. I suppose CONFIG_PPPOE is the kernel option to check out.</p>
<p>The second thing is that pppd should have the rp-pppoe.so plugin available. Once again, I don&#8217;t think you&#8217;ll find a distribution package for pppd without it.</p>
<p>With these at hand, I changed the pppd command to:</p>
<pre>/usr/sbin/pppd <span class="punch">plugin rp-pppoe.so eth1</span> linkname ADSL-$$ user <span class="yadayada">"myusername@013net"</span> defaultroute netmask 255.0.0.0 noauth lcp-echo-interval 60 lcp-echo-failure 3 nobsdcomp usepeerdns</pre>
<p>That&#8217;s exactly the same as above, but instead of the &#8220;pty&#8221; option that calls an external script, I use the plugin to talk with eth1 directly. No pppoe executable to eat CPU, and the transmission speed goes easily up to &gt;900 Mb/s without any dramatic CPU consumption visible (&#8220;top&#8221; reports 8% system CPU at worst, and that&#8217;s global to all CPUs).</p>
<p>I also removed the options for setting MTU and MRU in the pppd command. ppp0 now presents an MTU of 1492, which I suppose is correct. I mean, why fiddle with this? And I ditched the &#8220;remotename&#8221; option too.</p>
<p>Once again, the ONT (&#8220;fiber bridge&#8221;) was connected to eth1.</p>
<h3>Samples of log output</h3>
<p>This is the comparison between pppd&#8217;s output with pppoe as an executable and with the kernel&#8217;s pppoe module:</p>
<p>First, the old way, with pppoe executable:</p>
<pre>Using interface ppp0
<span class="punch">Connect: ppp0 &lt;--&gt; /dev/pts/13</span>
PAP authentication succeeded
local  IP address 109.186.24.16
remote IP address 212.143.8.104
primary   DNS address 194.90.0.1
secondary DNS address 212.143.0.1</pre>
<p>And with pppoe inside the kernel:</p>
<pre>Plugin rp-pppoe.so loaded.
RP-PPPoE plugin version 3.8p compiled against pppd 2.4.5
PPP session is 3865
Connected to 00:1a:f0:87:12:34 via interface eth1
Using interface ppp0
<span class="punch">Connect: ppp0 &lt;--&gt; eth1</span>
PAP authentication succeeded
peer from calling number 00:1A:F0:87:12:34 authorized
local  IP address 109.186.4.18
remote IP address 212.143.8.104
primary   DNS address 194.90.0.1
secondary DNS address 212.143.0.1</pre>
<p>The MAC address that is mentioned seems to be owned by Alcatel-Lucent, and is neither my own host&#8217;s or the ONT&#8217;s (i.e. the &#8220;fiber adapter&#8221;). It appears to belongs to the link partner over the fiber connection.</p>
<p>And by the way, if the ISP credentials are incorrect, the row saying &#8220;Connect X &lt;&#8211;&gt; Y&#8221; is followed by &#8220;LCP: timeout sending Config-Requests&#8221; after about 30 seconds. Instead of &#8220;PAP authentication succeeded&#8221;, of course.</p>
<h3>Clamping MSS</h3>
<p>The pppoe user-space utility had this nice &#8220;-m&#8221; option that caused all TCP packets with a SYN to be mangled, so that their MSS field was set as required for the pppoe link. But now I&#8217;m not using it anymore. How will the MSS field be correct now?</p>
<p>First of all, this is not an issue for packets that are created on the same computer that runs pppd. ppp0&#8242;s MTU is checked by the TCP stack, and the MSS is set correctly to 1452.</p>
<p>But forwarded packets come from a source that doesn&#8217;t know about ppp0&#8242;s reduced MTU. That host sets the MSS according to the NIC that it sees. It can&#8217;t know that this MSS may be too large for the pppoe link in the middle.</p>
<p>The solution is to add a rule in the firewall that mangles these packets:</p>
<pre>iptables -A FORWARD -o ppp0 -t mangle -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu</pre>
<p>This is more or less copied from iptables&#8217; man page. I added the -o part, because this is relevant only for packets going out to ppp0. No point mangling all forwarded packets.</p>
<h3>A wireshark dump</h3>
<p>This is what wireshark shows on the Ethernet card that is connected to the ONT during a successful connection to the ISP. It would most likely have looked the same on an ADSL link.</p>
<pre>No.     Time           Source                Destination           Protocol Length Info
      3 0.142500324    Dell_11:22:33         Broadcast             PPPoED   32     Active Discovery Initiation (PADI)
      4 0.144309286    Alcatel-_87:12:34     Dell_11:22:33         PPPoED   60     Active Discovery Offer (PADO) AC-Name='203'
      5 0.144360515    Dell_11:22:33         Alcatel-_87:12:34     PPPoED   52     Active Discovery Request (PADR)
      6 0.146062649    Alcatel-_87:12:34     Dell_11:22:33         PPPoED   60     Active Discovery Session-confirmation (PADS)
      7 0.147037263    Dell_11:22:33         Alcatel-_87:12:34     PPP LCP  36     Configuration Request
      8 0.192272315    Alcatel-_87:12:34     Dell_11:22:33         PPP LCP  60     Configuration Request
      9 0.192290554    Alcatel-_87:12:34     Dell_11:22:33         PPP LCP  60     Configuration Ack
     10 0.192335094    Dell_11:22:33         Alcatel-_87:12:34     PPP LCP  40     Configuration Ack
     11 0.192516908    Dell_11:22:33         Alcatel-_87:12:34     PPP LCP  30     Echo Request
     12 0.192660752    Dell_11:22:33         Alcatel-_87:12:34     PPP PAP  50     Authenticate-Request (Peer-ID='myusername@013net', Password='mypassword')
     13 0.201978697    Alcatel-_87:12:34     Dell_11:22:33         PPP LCP  60     Echo Reply
     14 0.309272346    Alcatel-_87:12:34     Dell_11:22:33         PPP PAP  60     Authenticate-Ack (Message='')
     15 0.309286268    Alcatel-_87:12:34     Dell_11:22:33         PPP IPCP 60     Configuration Request
     16 0.309289064    Alcatel-_87:12:34     Dell_11:22:33         PPP IPV6CP 60     Configuration Request
     17 0.309398416    Dell_11:22:33         Alcatel-_87:12:34     PPP IPCP 44     Configuration Request
     18 0.309429731    Dell_11:22:33         Alcatel-_87:12:34     PPP IPCP 32     Configuration Ack
     19 0.309441755    Dell_11:22:33         Alcatel-_87:12:34     PPP LCP  42     Protocol Reject
     20 0.315313539    Alcatel-_87:12:34     Dell_11:22:33         PPP IPCP 60     Configuration Nak
     21 0.315365821    Dell_11:22:33         Alcatel-_87:12:34     PPP IPCP 44     Configuration Request
     22 0.321070570    Alcatel-_87:12:34     Dell_11:22:33         PPP IPCP 60     Configuration Ack</pre>
<p>These &#8220;Configuration Request&#8221; and &#8220;Configuration Ack&#8221; packets contain a lot of data, of course: This is where the local and remote IP addresses are given, as well as the addresses to the DNSes.</p>
<h3>Some random notes</h3>
<ul>
<li>On a typical LAN connection over Ethernet, MSS is set to 1460. The typical value for a pppoe connection is 1452, 8 bytes lower.</li>
<li>Add &#8220;nodetach&#8221; to pppd&#8217;s command for a (debug) foreground session.</li>
<li>Add &#8220;dump&#8221; to pppd&#8217;s command to see all options in effect (from option file and command line combined).</li>
</ul>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2023/07/linux-kernel-pppoe-pppd/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Google Chrome: Stop that nagging on updates</title>
		<link>https://billauer.se/blog/2023/06/google-chrome-disable-update-popup/</link>
		<comments>https://billauer.se/blog/2023/06/google-chrome-disable-update-popup/#comments</comments>
		<pubDate>Sun, 11 Jun 2023 08:53:01 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Internet]]></category>
		<category><![CDATA[Software]]></category>
		<category><![CDATA[stop updates]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=6892</guid>
		<description><![CDATA[I have Google Chrome installed on a Linux machine at /opt/google as root, so the browser can&#8217;t update itself automatically. Instead, it complains with this pop-up every time the browser is started: What I really like about this pop-up is the &#8220;you&#8217;re missing out&#8221; part. I get the same thing from the silly image gallery [...]]]></description>
			<content:encoded><![CDATA[<p>I have Google Chrome installed on a Linux machine at /opt/google as root, so the browser can&#8217;t update itself automatically. Instead, it complains with this pop-up every time the browser is started:</p>
<p><a href="https://billauer.se/blog/wp-content/uploads/2023/06/reinstall.jpg"><img class="aligncenter size-full wp-image-6893" title="Annoying Google Chrome update popup" src="https://billauer.se/blog/wp-content/uploads/2023/06/reinstall.jpg" alt="Annoying Google Chrome update popup" width="324" height="180" /></a></p>
<p>What I really like about this pop-up is the &#8220;you&#8217;re missing out&#8221; part. I get the same thing from the silly image gallery app on my Google Pixel phone.  This is Google trying to play on my (not so existent) FOMO.</p>
<p>It has been <a rel="noopener" href="https://stackoverflow.com/questions/27962454/disable-chrome-is-out-of-date-notification" target="_blank">suggested</a> to add the &#8211;simulate-outdated-no-au argument to the command line that executes Chrome. This works indeed. The common suggestion is however to do that on the shortcut that executes the browser. But that won&#8217;t cover the case when I run the browser from a shell. Something I do, every now and then. Don&#8217;t ask.</p>
<p>So a more sledge hammer solution is to edit the wrapper script:</p>
<pre>$ which google-chrome
/usr/bin/google-chrome</pre>
<p>So edit this file (as root), and change the last line from</p>
<pre>exec -a "$0" "$HERE/chrome" "$@"</pre>
<p>to</p>
<pre>exec -a "$0" "$HERE/chrome" <span class="punch">--simulate-outdated-no-au='Tue, 31 Dec 2099'</span> "$@"</pre>
<p>What does this mean, then? Well, according to the list of <a rel="noopener" href="https://chromium.googlesource.com/chromium/src/+/HEAD/chrome/common/chrome_switches.cc" target="_blank">Google Chrome switches</a>, this switch &#8220;simulates that current version is outdated and auto-update is off&#8221;. The date is referred to in the source&#8217;s <a rel="noopener" href="https://source.chromium.org/chromium/chromium/src/+/HEAD:chrome/browser/upgrade_detector/upgrade_detector_impl.cc" target="_blank">upgrade_detector_impl.cc</a>. Look there if you want to figure out why this works (I didn&#8217;t bother, actually).</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2023/06/google-chrome-disable-update-popup/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Using git send-email with Gmail + OAUTH2, but without subscribing to cloud services</title>
		<link>https://billauer.se/blog/2022/10/git-send-email-with-oauth2-gmail/</link>
		<comments>https://billauer.se/blog/2022/10/git-send-email-with-oauth2-gmail/#comments</comments>
		<pubDate>Sun, 30 Oct 2022 09:08:44 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[email]]></category>
		<category><![CDATA[Internet]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[perl]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=6761</guid>
		<description><![CDATA[Introduction There is a widespread belief, that in order to use git send-email with Gmail, there&#8217;s a need to subscribe to Google Cloud services and obtain some credentials. Or that a two-factor authentication (2fa) is required. This is not the case, however. If Thunderbird can manage to fetch and send emails through Google&#8217;s mail servers [...]]]></description>
			<content:encoded><![CDATA[<h3>Introduction</h3>
<p>There is a widespread belief, that in order to use git send-email with Gmail, there&#8217;s a need to subscribe to Google Cloud services and obtain some credentials. Or that a two-factor authentication (2fa) is required.</p>
<p>This is not the case, however. If Thunderbird can manage to fetch and send emails through Google&#8217;s mail servers (as well as other OAUTH2 authenticated mail services), there&#8217;s no reason why a utility won&#8217;t be able to do the same.</p>
<p>The subscription to Google&#8217;s services is indeed required if the communication with Google&#8217;s server must be done without human supervision. That&#8217;s the whole point with API keys. If a human is around when the mail is dispatched, there&#8217;s no need for any special measures. And it&#8217;s quite obvious that there&#8217;s a responsive human around when a patch is being submitted.</p>
<p>What is actually needed, is a client ID and a client secret, and these are indeed obtained by registering to Google&#8217;s cloud service (<a href="https://gitlab.com/fetchmail/fetchmail/-/blob/e92e57cb1ce93b5a09509e65f26bbb5aee5de533/README.OAUTH2" target="_blank">this</a> explains how). But here&#8217;s the thing: Someone at Mozilla has already obtained these, and hardcoded them into Thunderbird itself. So there&#8217;s no problem using these to access Gmail with another mail client. It seems like many believe that the client ID and secret must be related to the mail account to access, and therefore each and every one has to obtain their own pair. That&#8217;s a mistake that has made a lot of people angry for nothing.</p>
<p>This post describes how to use git send-email without any further involvement with Google, except for having a Gmail account. The same method surely applies for other mail service providers that rely on OAUTH2, but I haven&#8217;t gotten into that. It should be quite easy to apply the same idea to other services as well however.</p>
<p>For this to work, Thunderbird must be configured to access the same email account. This doesn&#8217;t mean that you actually have to use Thunderbird for your mail exchange. It&#8217;s actually enough to configure the Gmail server as an <strong>outgoing</strong> mail server for the relevant account. In other words, you don&#8217;t even need to fetch mails from the server with Thunderbird.</p>
<p>The point is to make Thunderbird set up the OAUTH2 session, and then fetch the relevant piece of credentials from it. And take it from there with Google&#8217;s servers. Thunderbird is a good candidate for taking care of the session&#8217;s setup, because the whole idea with OAUTH2 is that the user / password session (plus possible additional authentication challenges) is done with a browser. Since Thunderbird is Firefox in disguise, it integrates the browser session well into its general flow.</p>
<p>If you want to use another piece of software to maintain the OAUTH2 session, that&#8217;s most likely possible, given that you can get its refresh token. This will also require obtaining its client ID and client secret. Odds are that it can be found somewhere in that software&#8217;s sources, exactly as I found it for Thunderbird. Or look at the https connection it runs to get an access token (which isn&#8217;t all that easy, encryption and that).</p>
<h3>Outline of solution</h3>
<p>All below relates to Linux Mint 19, Thunderbird 91.10.0, git version 2.17.1, Perl 5.26 and msmtp 1.8.14. But except for Thunderbird and msmtp, I don&#8217;t think the versions are going to matter.</p>
<p>It&#8217;s highly recommended to read through my <a rel="noopener" href="https://billauer.se/blog/2022/06/fetchmail-gmail-lsa-oauth2/" target="_blank">blog post on OAUTH2</a>, in particular the section called &#8220;The authentication handshake in a nutshell&#8221;. You&#8217;re going to need to know the difference between an access token and a refresh token sooner or later.</p>
<p>So the first obstacle is the fact that git send-email relies on the system&#8217;s sendmail to send out the emails. That utility doesn&#8217;t support OAUTH2 at the time of writing this. So instead, I used msmtp, which is a drop-in replacement for sendmail, plus it supports OAUTH2 (since version 1.8.13).</p>
<p>msmtp identifies itself to the server by sending it an access token in the SMTP session (see a dump of a sample session below). This access token is short-lived (3600 seconds from Google as of writing this), so it can&#8217;t be fetched from Thunderbird just like that. In particular because most of the time Thunderbird doesn&#8217;t have it.</p>
<p>What Thunderbird does have is a refresh token. It&#8217;s a completely automatic task to ask Google&#8217;s server for the access token with the refresh token at hand. It&#8217;s also an easy task (once you&#8217;ve figured out how to do it, that is). It&#8217;s also easy to get the refresh token from Thunderbird, exactly in the same way as getting a saved password. In fact, Thunderbird treats the refresh token as a password.</p>
<p>msmtp allows executing an arbitrary program in order to get the password or the access token. So I wrote a Perl script (<a rel="noopener" href="https://github.com/billauer/oauth2-helper/blob/main/oauth2-helper.pl" target="_blank">oauth2-helper.pl</a>) that reads the refresh token from a file and gets an access token from Google&#8217;s server. This is how msmtp manages to authenticate itself.</p>
<p>So everything relies on this refresh token. In principle, it can change every time it&#8217;s used. In practice, as of today, Google&#8217;s servers don&#8217;t change it. It seems like the refresh token is automatically replaced every six months, but even if that&#8217;s true today, it may change.</p>
<p>But that doesn&#8217;t matter so much. All that is necessary is that the refresh token is correct once. If the refresh token goes out of sync with Google&#8217;s server, a simple user / password session rectifies this. And as of now, than virtually never happens.</p>
<p>So let&#8217;s get to the hands-on part.</p>
<h3>Install msmtp</h3>
<p>Odds are that your distribution offers msmtp, so it can be installed with something like</p>
<pre># apt install msmtp</pre>
<p>Note however that the version needs to be at least 1.8.13, which wasn&#8217;t my case (Linux Mint 19). So I installed it from the sources. To do that, first install the TLS library, if it&#8217;s not installed already (as root):</p>
<pre># apt install gnutls-dev</pre>
<p>Then clone the git repository, compile and install:</p>
<pre>$ GIT_SSL_NO_VERIFY=true git clone http://git.marlam.de/git/msmtp.git
$ cd msmtp
$ git checkout msmtp-1.8.14
$ autoreconf -i
$ ./configure
$ make &amp;&amp; echo Success
$ sudo make install</pre>
<p>The installation goes to /usr/local/bin and other /usr/local/ paths, as one would expect.</p>
<p>I checked out version 1.8.14 because later versions failed to compile on my Linux Mint 19. OAUTH2 support was added in 1.8.13, and judging by the commit messages it hasn&#8217;t been changed since, except for commit 1f3f4bfd098, which is &#8220;Send XOAUTH2 in two lines, required by Microsoft servers&#8221;. Possibly cherry-pick this commit (I didn&#8217;t).</p>
<p>Once everything has been set up as described below, it&#8217;s possible to send an email with</p>
<pre>$ msmtp -v -t &lt; ~/email.eml</pre>
<p>The -v flag is used only for debugging, and it prints out the entire SMTP session.</p>
<p>The -t flag tells msmtp to fetch the recipients from the mail&#8217;s own headers. Otherwise, the recipients need to be listed in the command line, just like sendmail. Without this flag or recipients, msmtp just replies with</p>
<pre>msmtp: no recipients found</pre>
<p>The -t flag isn&#8217;t necessary with git send-email, because it explicitly lists the recipients in the command line.</p>
<h3>The oauth2-helper.pl script</h3>
<p>As mentioned above, Thunderbird has the refresh token, but msmtp needs an access token. So the script that talks with Google&#8217;s server and grabs the access token can be downloaded from its <a rel="noopener" href="https://github.com/billauer/oauth2-helper/blob/main/oauth2-helper.pl" target="_blank">Github repo</a>. Save it, with execution permission to /usr/local/bin/oauth2-helper.pl (or whatever, but this is what I assume in the configurations below).</p>
<p>Some Perl libraries may be required to run this script. On a Debian-based system, the packages&#8217; names are  probably something like libhttp-message-perl, libwww-perl and libjson-perl.</p>
<p>It&#8217;s written to access Google&#8217;s token server, but can be modified easily to access a different service provider by changing the parameters at its beginning. For other email providers, check if it happens to be listed in <a rel="noopener" href="https://github.com/mozilla/releases-comm-central/blob/master/mailnews/base/src/OAuth2Providers.sys.mjs" target="_blank">OAuth2Providers.sys.mjs</a>. I don&#8217;t know how well it will work with those other providers, though.</p>
<p>The script reads the refresh token from ~/.oauth2_reftoken as a plain file containing the blob only. There&#8217;s an inherent security risk of having this token stored like this, but it&#8217;s basically the same risk as the fact that it can be obtained from Thunderbird&#8217;s credential files. The difference is the amount of security by obscurity. Anyhow, the reference token isn&#8217;t your password, and it can&#8217;t be derived from it. Either way, make sure that this file has a 0600 or 0400 permission, if you&#8217;re running on a multi-user computer.</p>
<p>The script caches the access token in ~/.oauth2_acctoken, with an expiration timestamp. As of today, it means that the script talks with the Google&#8217;s server once in 60 minutes at most.</p>
<h3>Setting up config files</h3>
<p>So with msmtp installed and the script downloaded into /usr/local/bin/oauth2-helper.pl, all that is left is configuration files.</p>
<p>First, create ~/.msmtprc as follows (put your Gmail username instead of mail.username, of course):</p>
<pre>account default
host smtp.gmail.com
port 587
tls on
tls_starttls on
auth xoauth2
user mail.username
passwordeval /usr/local/bin/oauth2-helper.pl
from mail.username@gmail.com</pre>
<p>And then change the [sendemail] section in ~/.gitconfig to</p>
<pre>[sendemail]
        smtpServer = /usr/local/bin/msmtp</pre>
<p>That&#8217;s it. Only that single line. It&#8217;s however possible to use smtpServerOption in the .gitconfig to add various flags. So for example, to get the entire SMTP session shown while sending the email, it should say:</p>
<pre>[sendemail]
        smtpServer = /usr/local/bin/msmtp
        smtpServerOption = <span class="punch">-v
</span></pre>
<p>But really, don&#8217;t, unless there&#8217;s a problem sending mails.</p>
<p>Other than that, don&#8217;t keep old settings. For example, there should <strong>not</strong> be a &#8220;from=&#8221; entry in .gitconfig. Having such causes a &#8220;From:&#8221; header to be added into the mail body (so it&#8217;s visible to the reader of the mail). This header is created when there is a difference between the &#8220;From&#8221; that is generated by git send-email (which is taken from the &#8220;from=&#8221; entry) and the patch&#8217; author, as it appears in the patch&#8217; &#8220;From&#8221; header. The purpose of this in-body header is to tell &#8220;git am&#8221; who the real author is (i.e. not the sender of the patch). So this extra header won&#8217;t appear in the commit, but it nevertheless makes the sender of the message look somewhat clueless.</p>
<p>So in short, no old junk.</p>
<h3>Sending a patch</h3>
<p>Unless it&#8217;s the first time, I suggest just trying to send the patch to your own email address, and see if it works. There&#8217;s a good chance that the refresh token from the previous time will still be good, so it will just work, and no point hassling more.</p>
<p>Actually, it&#8217;s fine to try like this even on the first time, because the Perl script will fail to grab the access token and then tell you what to do to fix it, namely:</p>
<ul>
<li>Make sure that Thunderbird has access to the mail account itself, possibly by attempting to send an email through Gmail&#8217;s server.</li>
<li>Go to Thunderbird&#8217;s Preferences &gt; Privacy &amp; Security and click on Saved Passwords. Look for the account, where the Provider start with oauth://. Right-click that line and choose &#8220;Copy Password&#8221;.</li>
<li>Create or open ~/.oauth2_reftoken, and paste the blob into that file, so it contains only that string. No need to be uptight with newlines and whitespaces: They are ignored.</li>
</ul>
<p>And then go, as usual:</p>
<pre>$ git send-email --to 'my@test.mail' 0001-my.patch</pre>
<p>I&#8217;ve added the output of a successful session (with the -v flag) below.</p>
<h3>Room for improvements</h3>
<p>It would have been nicer to fetch the refresh token automatically from Thunderbird&#8217;s credentials store (that is from logins.json, based upon the decryption key that is kept in key4.db), but the available scripts for that are written in Python. And to me Python is equal to &#8220;will cause trouble sooner or later&#8221;. Anyhow, <a rel="noopener" href="https://apr4h.github.io/2019-12-20-Harvesting-Browser-Credentials/" target="_blank">this tutorial</a> describes the mechanism (in the part about Firefox).</p>
<p>Besides, it could have been even nicer if the script was completely standalone, and didn&#8217;t depend on Thunderbird at all. That requires doing the whole dance with the browser, something I have no motivation to get into.</p>
<h3>A successful session</h3>
<p>This is what it looks like when a patch is properly sent, with the smtpServerOption = -v line in .gitignore (so msmtp produces verbose output):</p>
<pre><span class="yadayada">Send this email? ([y]es|[n]o|[q]uit|[a]ll): y</span>
ignoring system configuration file /usr/local/etc/msmtprc: No such file or directory
loaded user configuration file /home/eli/.msmtprc
falling back to default account
Fetching access token based upon refresh token in /home/eli/.oauth2_reftoken...
using account default from /home/eli/.msmtprc
host = smtp.gmail.com
port = 587
source ip = (not set)
proxy host = (not set)
proxy port = 0
socket = (not set)
timeout = off
protocol = smtp
domain = localhost
auth = XOAUTH2
user = mail.username
password = *
passwordeval = /usr/local/bin/oauth2-helper.pl
ntlmdomain = (not set)
tls = on
tls_starttls = on
tls_trust_file = system
tls_crl_file = (not set)
tls_fingerprint = (not set)
tls_key_file = (not set)
tls_cert_file = (not set)
tls_certcheck = on
tls_min_dh_prime_bits = (not set)
tls_priorities = (not set)
tls_host_override = (not set)
auto_from = off
maildomain = (not set)
from = mail.username@gmail.com
set_from_header = auto
set_date_header = auto
remove_bcc_headers = on
undisclosed_recipients = off
dsn_notify = (not set)
dsn_return = (not set)
logfile = (not set)
logfile_time_format = (not set)
syslog = (not set)
aliases = (not set)
reading recipients from the command line
&lt;-- 220 smtp.gmail.com ESMTP m8-20020a7bcb88000000b003c6d21a19a0sm3316430wmi.29 - gsmtp
--&gt; EHLO localhost
&lt;-- 250-smtp.gmail.com at your service, [109.186.183.118]
&lt;-- 250-SIZE 35882577
&lt;-- 250-8BITMIME
&lt;-- 250-STARTTLS
&lt;-- 250-ENHANCEDSTATUSCODES
&lt;-- 250-PIPELINING
&lt;-- 250-CHUNKING
&lt;-- 250 SMTPUTF8
--&gt; STARTTLS
&lt;-- 220 2.0.0 Ready to start TLS
TLS session parameters:
    (TLS1.2)-(ECDHE-ECDSA-SECP256R1)-(CHACHA20-POLY1305)
TLS certificate information:
    Subject:
        CN=smtp.gmail.com
    Issuer:
        C=US,O=Google Trust Services LLC,CN=GTS CA 1C3
    Validity:
        Activation time: Mon 26 Sep 2022 11:22:04 AM IDT
        Expiration time: Mon 19 Dec 2022 10:22:03 AM IST
    Fingerprints:
        SHA256: 53:F3:CA:1D:37:F2:1F:ED:2C:67:40:A2:A2:29:C2:C8:E8:AF:9E:60:7A:01:92:EC:F0:2A:11:E8:37:A5:88:F3
        SHA1 (deprecated): D4:69:6E:59:2D:75:43:59:02:74:25:67:E7:57:40:E0:28:43:A8:62
--&gt; EHLO localhost
&lt;-- 250-smtp.gmail.com at your service, [109.186.183.118]
&lt;-- 250-SIZE 35882577
&lt;-- 250-8BITMIME
&lt;-- 250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH
&lt;-- 250-ENHANCEDSTATUSCODES
&lt;-- 250-PIPELINING
&lt;-- 250-CHUNKING
&lt;-- 250 SMTPUTF8
--&gt; AUTH XOAUTH2 dXNlcj1lbGkuYmlsbGF1ZXIBYXV0aD1CZWFyZXIgeWEyOS5hMEFhNHhyWE1GM1gtOTJMVWNidjE4MFdVOBROENRcUdSbk5KaUFSY0VSckVaXzdzbDlHMTNpdFIyUTk0NjlKWG45aHVGLQVRBU0FSTVXJpSjRqMjBLcWh6WU9GekxlcU5BYVpFNUU4WXRhNjdLUXpCRm1HRDg3dFgzeHJ4amNPTnRVTkZFVWdESXhsUlcxOFhVT0pqQ1hPSlFwZlNGUUVqRHZMOWw4RExkTjlKZlNbGRTazNNbFNMNjVfQWFDZ1lLVVF2Y0luOWNSSUEwMTY2AQE=
&lt;-- 235 2.7.0 Accepted
--&gt; MAIL FROM:&lt;mail.username@gmail.com&gt;
--&gt; RCPT TO:&lt;test@mail.com&gt;
--&gt; RCPT TO:&lt;mail.username@gmail.com&gt;
--&gt; DATA
&lt;-- 250 2.1.0 OK m8-20020a7bcb88000000b003c6d21a19a0sm3316430wmi.29 - gsmtp
&lt;-- 250 2.1.5 OK m8-20020a7bcb88000000b003c6d21a19a0sm3316430wmi.29 - gsmtp
&lt;-- 250 2.1.5 OK m8-20020a7bcb88000000b003c6d21a19a0sm3316430wmi.29 - gsmtp
&lt;-- 354  Go ahead m8-20020a7bcb88000000b003c6d21a19a0sm3316430wmi.29 - gsmtp
--&gt; From: Eli Billauer &lt;mail.username@gmail.com&gt;
--&gt; To: test@mail.com
--&gt; Cc: Eli Billauer &lt;mail.username@gmail.com&gt;
--&gt; Subject: [PATCH v8] Gosh! Why don't you apply this patch already!
--&gt; Date: Sun, 30 Oct 2022 07:01:14 +0200
--&gt; Message-Id: &lt;20221030050114.49299-1-mail.username@gmail.com&gt;
--&gt; X-Mailer: git-send-email 2.17.1
--&gt; 

<span class="yadayada">[ ... email body comes here ... ]</span>

--&gt; --
--&gt; 2.17.1
--&gt;
--&gt; .
&lt;-- 250 2.0.0 OK  1667106108 m8-20020a7bcb88000000b003c6d21a19a0sm3316430wmi.29 - gsmtp
--&gt; QUIT
&lt;-- 221 2.0.0 closing connection m8-20020a7bcb88000000b003c6d21a19a0sm3316430wmi.29 - gsmtp
OK. Log says:
Sendmail: /usr/local/bin/msmtp -v -i test@mail.com mail.username@gmail.com
From: Eli Billauer &lt;mail.username@gmail.com&gt;
To: test@mail.com
Cc: Eli Billauer &lt;mail.username@gmail.com&gt;
Subject: [PATCH v8] Gosh! Why don't you apply this patch already!
Date: Sun, 30 Oct 2022 07:01:14 +0200
Message-Id: &lt;20221030050114.49299-1-mail.username@gmail.com&gt;
X-Mailer: git-send-email 2.17.1

Result: OK</pre>
<p>Ah, and the fact that the access token can be copied from here is of course meaningless, as it has expired long ago.</p>
<h3>Thunderbird debug notes</h3>
<p>These are some random notes I made while digging in Thunderbird&#8217;s guts to find out what&#8217;s going on.</p>
<p>So this is Thunderbird&#8217;s official <a rel="noopener" href="https://github.com/mozilla/releases-comm-central" target="_blank">git repo</a>. Not that I used it.</p>
<p>To get logging info from Thunderbird: Based upon <a rel="noopener" href="https://wiki.mozilla.org/MailNews:Logging#Setting_Thunderbird_Preference" target="_blank">this page</a>, go to Thunderbird&#8217;s preferences &gt; General and click the Config Editor button. Set mailnews.oauth.loglevel to All (was Warn). Same with mailnews.smtp.loglevel. Then open the Error Console with Ctrl+Shift+J.</p>
<p>The cute thing about these logs is that the access code is written in the log. So it&#8217;s possible to skip the Perl script, and use the access code from Thunderbird&#8217;s log. Really inconvenient, but possible.</p>
<p>The OAuth2 token requests is implemented in <a rel="noopener" href="https://github.com/mozilla/releases-comm-central/blob/master/mailnews/base/src/OAuth2.jsm" target="_blank">Oauth2.jsm</a>. It&#8217;s possible to make a breakpoint in this module by through Tools &gt; Developer Tools &gt; Developer Toolbox, and once it opens (after requesting permission for external connection), go to the debugger.</p>
<p>Find Oauth2.jsm in the sources pane to the left (of the Debugger tab), under resource:// modules &gt; sessionstore. Add a breakpoint in requestAccessToken() so that the clientID and consumerSecret properties can be revealed.</p>
<h3><span style="color: #888888;">Sending a patch from Thunderbird directly</span></h3>
<p><span style="color: #888888;">This is a really bad idea. But if you have Thunderbird, and need to send a patch right now, this is a quick, dirty and somewhat dangerous procedure for doing that.</span></p>
<p><span style="color: #888888;">Why is it dangerous? Because at some point, it&#8217;s easy to pick &#8220;Send now&#8221; instead of &#8220;Send later&#8221;, and boom, a junk patch is mailed to the whole world.</span></p>
<p><span style="color: #888888;">The problem with Thunderbird is that it makes small changes into the patch&#8217; body. So to work around this, there&#8217;s a really silly procedure. I used it once, and I&#8217;m not proud of that.</span></p>
<p><span style="color: #888888;">So here we go.</span></p>
<p><span style="color: #888888;">First, a very simple script that outputs the patch mail into a file. Say that I called it dumpit (should be executable, of course):</span></p>
<pre><span style="color: #888888;">#!/bin/bash

cat &gt; /home/eli/Desktop/git-send-email.eml
</span></pre>
<p><span style="color: #888888;">Then change ~/.gitconfig, so it reads something like this in the [sendemail] section:</span></p>
<pre><span style="color: #888888;">[sendemail]
        from = mail.username@gmail.com
        smtpServer = /home/eli/Desktop/dumpit
</span></pre>
<p><span style="color: #888888;">So basically it uses the silly script as a mail server, and the content goes out to a plain file.</span></p>
<p><span style="color: #888888;">Then run git send-email as usual. The result is a git-send-email.eml as a file.</span></p>
<p><span style="color: #888888;">And now comes the part of making Thunderbird send it.</span></p>
<ul>
<li><span style="color: #888888;">Close Thunderbird. All windows.</span></li>
<li><span style="color: #888888;">Change directory to where Thunderbird keeps its profile files, to under Mail/Local Folders</span></li>
<li><span style="color: #888888;">Remove &#8220;Unsent Messages&#8221; and &#8220;Unsent Messages.msf&#8221;</span></li>
<li><span style="color: #888888;">Open Thunderbird again</span></li>
<li><span style="color: #888888;">Inside Thunderbird, go to Hamburger Icon &gt; File &gt; Open &gt; Saved Message&#8230; and select git-send-email.eml. The email message should appear.</span></li>
<li><span style="color: #888888;">Right-Click somewhere in the message&#8217;s body, and pick Edit as New Message&#8230;</span></li>
<li><span style="color: #888888;"><strong>Don&#8217;t send this message as is</strong>! It&#8217;s completely messed up. In particular, there are some indentations in the patch itself, which renders it useless.</span></li>
<li><span style="color: #888888;">Instead, pick File &gt; Send Later.</span></li>
<li><span style="color: #888888;">Once again, close Thunderbird. All windows.</span></li>
<li><span style="color: #888888;">Remove &#8220;Unsent Messages.msf&#8221; (only)</span></li>
<li><span style="color: #888888;">Edit &#8220;Unsent Messages&#8221; as follows: Everything under the &#8220;Content-Transfer-Encoding: 7bit&#8221; part is the mail&#8217;s body. So remove the &#8220;From:&#8221; line after it, and paste the email&#8217;s body from git-send-email.eml instead.</span></li>
<li><span style="color: #888888;">Note that there are normally two blank lines after the mail&#8217;s body. Retain them.</span></li>
<li><span style="color: #888888;">Open Thunderbird again. Verify that those indentations are away.</span></li>
<li><span style="color: #888888;">Look at the mail inside Outbox, and verify that it&#8217;s OK now. These are the three things to look for in particular:</span>
<ul>
<li><span style="color: #888888;">The &#8220;From:&#8221; part at the beginning of the message is gone.</span></li>
<li><span style="color: #888888;">At the end of the message, there&#8217;s a &#8220;&#8211;&#8221; and git&#8217;s version number. These should be in <strong>separate lines</strong>.</span></li>
<li><span style="color: #888888;">Look at the mail&#8217;s source. The &#8220;+&#8221; and &#8220;-&#8221; signs of the diffs must not be indented.</span></li>
</ul>
</li>
<li><span style="color: #888888;">If all is fine, right-click Outbox, and pick &#8220;Send unsent messages&#8221;. And hope for good.</span></li>
</ul>
<p><span style="color: #888888;">Are you sure you want to do this?</span></p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2022/10/git-send-email-with-oauth2-gmail/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Blocking bots by their IP addresses, the DIY version</title>
		<link>https://billauer.se/blog/2022/08/spiders-bots-denial-iptables-ipset/</link>
		<comments>https://billauer.se/blog/2022/08/spiders-bots-denial-iptables-ipset/#comments</comments>
		<pubDate>Tue, 16 Aug 2022 10:26:37 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Internet]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[perl]]></category>
		<category><![CDATA[Server admin]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=6670</guid>
		<description><![CDATA[Introduction I had some really annoying bots on one of my websites. Of the sort that make a million requests (like really, a million) per month, identifying themselves as a browser. So IP blocking it is. I went for a minimalistic DIY approach. There are plenty of tools out there, but my experience with things [...]]]></description>
			<content:encoded><![CDATA[<h3>Introduction</h3>
<p>I had some really annoying bots on one of my websites. Of the sort that make a million requests (like really, a million) per month, identifying themselves as a browser.</p>
<p>So IP blocking it is. I went for a minimalistic DIY approach. There are plenty of tools out there, but my experience with things like this is that in the end, it&#8217;s me and the scripts. So I might as well write them myself.</p>
<h3>The IP set feature</h3>
<p>Iptables has an IP set module, which allows feeding it with a set of random IP addresses. Internally, it creates a hash with these addresses, so it&#8217;s an efficient way to keep track of multiple addresses.</p>
<p>IP sets has been in the kernel since ages, but it has to be opted in the kernel with CONFIG_IP_SET. Which it most likely is.</p>
<p>The ipset utility may need to be installed, with something like</p>
<pre># apt install ipset</pre>
<p>There seems to be a protocol mismatch issue with the kernel, which apparently is a non-issue. But every time something goes wrong with ipset, there&#8217;s a warning message about this mismatch, which is misleading. So it looks something like this.</p>
<pre># ipset <span class="yadayada">[ ... something stupid or malformed ... ]</span>
ipset v6.23: Kernel support protocol versions 6-7 while userspace supports protocol versions 6-6
<span class="yadayada">[ ... some error message related to the stupidity ... ]</span></pre>
<p>So the important thing is to be aware of is that odds are that the problem isn&#8217;t the version mismatch, but between chair and keyboard.</p>
<h3>Hello, world</h3>
<p>A quick session</p>
<pre># ipset create testset hash:ip
# ipset add testset 1.2.3.4
# iptables -I INPUT -m set --match-set testset src -j DROP
# ipset del testset 1.2.3.4</pre>
<p>Attempting to add an IP address that is already in the list causes a warning, and the address isn&#8217;t added. So no need to check if the address is already there. Besides, there the -exist option, which is really great.</p>
<p>List the members of the IP set:</p>
<pre># ipset -L</pre>
<h3>Timeout</h3>
<p>An entry can have a timeout feature, which works exactly as one would expect: The rule vanishes after the timeout expires. The timeout entry in ipset -L counts down.</p>
<p>For this to work, the set must be created with a default timeout attribute. Zero means that timeout is disabled (which I chose as a default in this example).</p>
<pre># ipset create testset hash:ip timeout 0
# ipset add testset 1.2.3.4 timeout 10</pre>
<p>The &#8216;-exist&#8217; flag causes ipset to re-add an existing entry, which also resets its timeout. So this is the way to keep the list fresh.</p>
<h3>Don&#8217;t put the DROP rule first</h3>
<p>It&#8217;s tempting to put the DROP rule with &#8211;match-set first, because hey, let&#8217;s give those intruders the boot right away. But doing that, there might be TCP connections lingering, because the last FIN packet is caught by the firewall as the new rule is added. Given that adding an IP address is the result of a flood of requests, this is a realistic scenario.</p>
<p>The solution is simple: There&#8217;s most likely a &#8220;state RELATED,ESTABLISHED&#8221; rule somewhere in the list. So push it to the top. The rationale is simple: If a connection has begun, don&#8217;t chop it in the middle in any case. It&#8217;s the first packet that we want killed.</p>
<h3>Persistence</h3>
<p>The rule in iptables must refer to an existing set. So if the rule that relies on the set is part of the persistent firewall rules, it must be created before the script that brings up iptables runs.</p>
<p>This is easily done by adding a rule file like this as /usr/share/netfilter-persistent/plugins.d/10-ipset</p>
<pre><span class="hljs-meta">#!/bin/sh</span>

IPSET=/sbin/ipset
SET=mysiteset

<span class="hljs-keyword">case</span> <span class="hljs-string">"<span class="hljs-variable">$1</span>"</span> <span class="hljs-keyword">in</span>
start|restart|reload|force-reload)
	<span class="hljs-variable">$IPSET</span> destroy
	<span class="hljs-variable">$IPSET</span> create <span class="hljs-variable">$SET</span> <span class="hljs-built_in">hash</span>:ip <span class="hljs-built_in">timeout</span> 0
	;;

save)
	<span class="hljs-built_in">echo</span> <span class="hljs-string">"ipset-persistent: The save option does nothing"</span>
	;;

stop|flush)
	<span class="hljs-variable">$IPSET</span> flush <span class="hljs-variable">$SET</span>
	;;
*)
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Usage: <span class="hljs-variable">$0</span> {start|restart|reload|force-reload|save|flush}"</span> &gt;&amp;2
    <span class="hljs-built_in">exit</span> 1
    ;;
<span class="hljs-keyword">esac</span>

<span class="hljs-built_in">exit</span> 0</pre>
<p>The idea is that the index 10 in the file&#8217;s name is smaller than the rule that sets up iptables, so it runs first.</p>
<p>This script is a dirty hack, but hey, it works. There&#8217;s a <a rel="noopener" href="https://sourceforge.net/p/ipset-persistent/wiki/Home/" target="_blank">small project</a> on this, for those who like to do it properly.</p>
<p>The operating system in question is systemd-based, but this old school style is still in effect.</p>
<h3>Maybe block by country?</h3>
<p>Since all offending requests came from the same country (cough, cough, China, from more than 4000 different IP addresses) I&#8217;m considering to block them in one go. A list of 4000+ IP addresses that I busted in August 2022 with aggressive bots (all from China) can be downloaded as a simple <a href="/download/china-bots-ip-addresses.txt.gz" target="_blank">compressed text file</a>.</p>
<p>So the idea is going something like</p>
<pre>ipset create foo hash:net
ipset add foo 192.168.0.0/24
ipset add foo 10.1.0.0/16
ipset add foo 192.168.0/24</pre>
<p>and download the per-country IP ranges from <a href="https://www.ipdeny.com/ipblocks/" target="_blank">IP deny</a>. That&#8217;s a simple and crude tool for denial by geolocation. The only thing that puts me down a bit is that it&#8217;s &gt; 7000 rules, so I wonder if that doesn&#8217;t put a load on the server. But what really counts is the number of sizes of submasks, because each submask size has its own hash. So if the list covers all possible  sizes, from a full /32 down to say, 16/, there are 17 hashes to look up for each packet arriving.</p>
<p>On the other hand, since the rule should be after the &#8220;state RELATED,ESTABLISHED&#8221; rule, it only covers SYN packets. And if this whole thing is put as late as possible in the list of rules, it boils down to handling only packets that are intended for the web server&#8217;s ports, or those that are going to be dropped anyhow. So compared with the CPU cycles of handling the http request, even 17 hashes isn&#8217;t all that much.</p>
<p>The biggest caveat is however if other websites are colocated on the server. It&#8217;s one thing to block offending IPs, but blocking a whole country from all sites, that&#8217;s a bit too much.</p>
<p><em>Note to self: In the end, I wrote a little Perl-XS module that says if the IP belongs to a group. Look for byip.pm.</em></p>
<h3>The blacklisting script</h3>
<p>The Perl script that performs the blacklisting is crude and inaccurate, but simple. This is the part to tweak and play with, and in particular adapt to each specific website. It&#8217;s all about detecting abnormal access.</p>
<p>Truth to be told, I replaced this script with a more sophisticated mechanism pretty much right away on my own system. But what&#8217;s really interesting is the calls to ipset.</p>
<p>This script reads through Apache&#8217;s access log file, and analyzes each minute in time (as in 60 seconds). In other words, all accesses that have the same timestamp, with the seconds part ignored. Note that the regex part that captures $time in the script ignores the last part of :\d\d.</p>
<p>If the same IP address appears more than 50 times, that address is blacklisted, with a timeout of 86400 seconds (24 hours). Log file that correspond to page requisites and such (images, style files etc.) are skipped for this purpose. Otherwise, it&#8217;s easy to reach 50 accesses within a minute with legit web browsing.</p>
<p>There are several imperfections about this script, among others:</p>
<ul>
<li>Since it reads through the entire log file each time, it keeps relisting each IP address until the access file is rotated away, and a new one is started. This causes an update of the timeout, so effectively the blacklisting takes place for up to 48 hours.</li>
<li>Looking in segments of accesses that happen to have the same minute in the timestamp is quite inaccurate regarding which IPs are caught and which aren&#8217;t.</li>
</ul>
<p>The script goes as follows:</p>
<pre><span class="hljs-comment">#!/usr/bin/perl</span>
<span class="hljs-keyword">use</span> warnings;
<span class="hljs-keyword">use</span> strict;

<span class="hljs-keyword">my</span> $logfile = <span class="hljs-string">'/var/log/mysite.com/access.log'</span>;
<span class="hljs-keyword">my</span> $limit = <span class="hljs-number">50</span>; <span class="hljs-comment"># 50 accesses per minute</span>
<span class="hljs-keyword">my</span> $timeout = <span class="hljs-number">86400</span>;

<span class="hljs-keyword">open</span>(<span class="hljs-keyword">my</span> $in, <span class="hljs-string">"&lt;"</span>, $logfile)
  <span class="hljs-keyword">or</span> <span class="hljs-keyword">die</span> <span class="hljs-string">"Can't open $logfile for read: $!\n"</span>;

<span class="hljs-keyword">my</span> $current = <span class="hljs-string">''</span>;
<span class="hljs-keyword">my</span> $l;
<span class="hljs-keyword">my</span> %h;
<span class="hljs-keyword">my</span> %blacklist;

<span class="hljs-keyword">while</span> (<span class="hljs-keyword">defined</span> ($l = &lt;$in&gt;)) {
  <span class="hljs-keyword">my</span> ($ip, $time, $req) = ($l =~ <span class="hljs-regexp">/^([^ ]+).*?\[(.+?):\d\d[ ].*?\"\w+[ ]+([^\"]+)/</span>);
  <span class="hljs-keyword">unless</span> (<span class="hljs-keyword">defined</span> $ip) {
    <span class="hljs-comment">#    warn("Failed to parse line $l\n");</span>
    <span class="hljs-keyword">next</span>;
  }

  <span class="hljs-keyword">next</span>
    <span class="hljs-keyword">if</span> ($req =~ <span class="hljs-regexp">/^\/(?:media\/|robots\.txt)/</span>);

  <span class="hljs-keyword">unless</span> ($time eq $current) {
    <span class="hljs-keyword">foreach</span> <span class="hljs-keyword">my</span> $k (<span class="hljs-keyword">sort</span> <span class="hljs-keyword">keys</span> %h) {
      $blacklist{$k} = <span class="hljs-number">1</span>
	<span class="hljs-keyword">if</span> ($h{$k} &gt;= $limit);
    }

    %h = ();
    $current = $time;
  }
  $h{$ip}++;
}

<span class="hljs-keyword">close</span> $in;

<span class="hljs-keyword">foreach</span> <span class="hljs-keyword">my</span> $k (<span class="hljs-keyword">sort</span> <span class="hljs-keyword">keys</span> %blacklist) {
  <span class="hljs-keyword">system</span>(<span class="hljs-string">'/sbin/ipset'</span>, <span class="hljs-string">'add'</span>, <span class="hljs-string">'-exist'</span>, <span class="hljs-string">'mysiteset'</span>, $k, <span class="hljs-string">'timeout'</span>, $timeout);
}</pre>
<p>It has to be run as root, of course. Most likely as a cronjob.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2022/08/spiders-bots-denial-iptables-ipset/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Google Translate, LaTeX and asian languages: Technical notes</title>
		<link>https://billauer.se/blog/2022/08/google-translate-pdflatex-technical/</link>
		<comments>https://billauer.se/blog/2022/08/google-translate-pdflatex-technical/#comments</comments>
		<pubDate>Mon, 15 Aug 2022 07:18:50 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Internet]]></category>
		<category><![CDATA[perl]]></category>
		<category><![CDATA[Software]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=6665</guid>
		<description><![CDATA[Introduction These post contains a few technical notes of using Google Translate for translating LaTeX documents into Chinese, Japanese and Korean. The insights on the language-related issues are written down in a separate post. Text vs. HTML Google&#8217;s cloud translator can be fed with either plain text or HTML, and it returns the same format. [...]]]></description>
			<content:encoded><![CDATA[<h3>Introduction</h3>
<p>These post contains a few technical notes of using Google Translate for translating LaTeX documents into Chinese, Japanese and Korean. The insights on the language-related issues are written down in a <a title="Translating technical documentation with Google Translate" href="https://billauer.se/blog/2022/08/google-translate-insights/" target="_blank">separate post</a>.</p>
<h3>Text vs. HTML</h3>
<p>Google&#8217;s cloud translator can be fed with either plain text or HTML, and it returns the same format. Plain text format is out of the question for anything but translating short sentences, as it becomes impossible to maintain the text&#8217;s formatting. So I went for the HTML interface.</p>
<p>The thing with HTML is that whitespaces can take different forms and shapes, and they are redundant in many situations. For example, a newline is often equivalent to a plain space, and neither make any difference between two paragraphs that are enclosed by &lt;p&gt; tags.</p>
<p>Google Translate takes this notion to the extreme, and typically removes all newlines from the original text. OK, that&#8217;s understandable. But it also adds and removes whitespaces where it had no business doing anything, in particular around meaningless segments that aren&#8217;t translated anyhow. This makes it quite challenging when feeding the results for further automatic processing.</p>
<h3>Setting up a Google Cloud account</h3>
<p>When creating a new Google Cloud account, there&#8217;s an automatic credit of $300 to spend for three months. So there&#8217;s plenty of room for much needed experimenting. Too see the status of the evaluation period, go to Billing &gt; Cost Breakdown and wait a minute or so for the &#8220;Free trial status&#8221; strip to appear at the top of the page. There&#8217;s no problem with &#8220;activating full account&#8221; immediately. The free trial credits remain, but it also means that real billing occurs when the credits are consumed and/or the trial period is over.</p>
<p>First create a new Google cloud account and enable the Google Translate API.</p>
<p>I went for Basic v2 translation (and not Advanced, v3). Their pricing is the same, but v3 is not allowed with an API key, and I really wasn&#8217;t into setting up a service account and struggle with OAuth2. The main advantage with v3 is the possibility to train the machine to adapt to a specific language pattern, but as mentioned in <a title="Translating technical documentation with Google Translate" href="https://billauer.se/blog/2022/08/google-translate-insights/" target="_blank">that separate post</a>, I&#8217;m hiding away anything but common English language patterns.</p>
<p>As for authentication, I went for <a rel="noopener" href="https://cloud.google.com/docs/authentication/api-keys" target="_blank">API keys</a>. I don&#8217;t need any personalized info, so that&#8217;s the simple way to go. To obtain the keys, go to main menu (hamburger icon) &gt; APIs and services &gt; Credentials and pick Create Credentials, and choose to create API keys. Copy the string and use it in the key=API_KEY parameters in POST requests. It&#8217;s possible to restrict the usage of this key in various ways (HTTP referrer, IP address etc.) but it wasn&#8217;t relevant in my case, because the script runs only on my computer.</p>
<p>The web interface for setting up cloud services is horribly slow, which is slightly ironic and a bit odd for a company like Google.</p>
<h3>The translation script</h3>
<p>I wrote a simple script for taking a piece of text in English and translating it into the language of choice:</p>
<pre><span class="hljs-comment">#!/usr/bin/perl</span>

<span class="hljs-keyword">use</span> warnings;
<span class="hljs-keyword">use</span> strict;
<span class="hljs-keyword">use</span> LWP::UserAgent;
<span class="hljs-keyword">use</span> JSON <span class="hljs-string">qw[ from_json ]</span>;

<span class="hljs-keyword">our</span> $WASTEMONEY = <span class="hljs-number">0</span>; <span class="hljs-comment"># Prompt before making request</span>
<span class="hljs-keyword">my</span> $MAXLEN = <span class="hljs-number">500000</span>;
<span class="hljs-keyword">my</span> $chars_per_dollar = <span class="hljs-number">50000</span>; <span class="hljs-comment"># $20 per million characters</span>

<span class="hljs-keyword">our</span> $APIkey = <span class="hljs-string">'your API key here'</span>;

<span class="hljs-keyword">my</span> ($outfile, $origfile, $lang) = @ARGV;

<span class="hljs-keyword">die</span>(<span class="hljs-string">"Usage: $0 outfile origfile langcode\n"</span>)
  <span class="hljs-keyword">unless</span> (<span class="hljs-keyword">defined</span> $origfile);

<span class="hljs-keyword">my</span> $input = readfile($origfile);

askuser() <span class="hljs-keyword">unless</span> ($WASTEMONEY);

<span class="hljs-keyword">my</span> $len = <span class="hljs-keyword">length</span> $input;

<span class="hljs-keyword">die</span>(<span class="hljs-string">"Cowardly refusing to translate $len characters\n"</span>)
  <span class="hljs-keyword">if</span> ($len &gt; $MAXLEN);

writefile($outfile, translate($input, $lang));

<span class="hljs-comment">################## SUBROUTINES ##################</span>

<span class="hljs-function"><span class="hljs-keyword">sub</span> <span class="hljs-title">writefile</span> </span>{
  <span class="hljs-keyword">my</span> ($fname, $data) = @_;

  <span class="hljs-keyword">open</span>(<span class="hljs-keyword">my</span> $out, <span class="hljs-string">"&gt;"</span>, $fname)
    <span class="hljs-keyword">or</span> <span class="hljs-keyword">die</span> <span class="hljs-string">"Can't open \"$fname\" for write: $!\n"</span>;
  <span class="hljs-keyword">binmode</span>($out, <span class="hljs-string">":utf8"</span>);
  <span class="hljs-keyword">print</span> $out $data;
  <span class="hljs-keyword">close</span> $out;
}

<span class="hljs-function"><span class="hljs-keyword">sub</span> <span class="hljs-title">readfile</span> </span>{
  <span class="hljs-keyword">my</span> ($fname) = @_;

  <span class="hljs-keyword">local</span> $/; <span class="hljs-comment"># Slurp mode</span>

  <span class="hljs-keyword">open</span>(<span class="hljs-keyword">my</span> $in, <span class="hljs-string">"&lt;"</span>, $fname)
    <span class="hljs-keyword">or</span> <span class="hljs-keyword">die</span> <span class="hljs-string">"Can't open $fname for read: $!\n"</span>;

  <span class="hljs-keyword">my</span> $input = &lt;$in&gt;;
  <span class="hljs-keyword">close</span> $in;

  <span class="hljs-keyword">return</span> $input;
}

<span class="hljs-function"><span class="hljs-keyword">sub</span> <span class="hljs-title">askuser</span> </span>{
  <span class="hljs-keyword">my</span> $len = <span class="hljs-keyword">length</span> $input;
  <span class="hljs-keyword">my</span> $cost = <span class="hljs-keyword">sprintf</span>(<span class="hljs-string">'$%.02f'</span>, $len / $chars_per_dollar);

  <span class="hljs-keyword">print</span> <span class="hljs-string">"\n\n*** Approval to access Google Translate ***\n"</span>;
  <span class="hljs-keyword">print</span> <span class="hljs-string">"$len bytes to $lang, $cost\n"</span>;
  <span class="hljs-keyword">print</span> <span class="hljs-string">"Source file: $origfile\n"</span>;
  <span class="hljs-keyword">print</span> <span class="hljs-string">"Proceed? [y/N] "</span>;

  <span class="hljs-keyword">my</span> $ans = &lt;STDIN&gt;;

  <span class="hljs-keyword">die</span>(<span class="hljs-string">"Aborted due to lack of consent to proceed\n"</span>)
    <span class="hljs-keyword">unless</span> ($ans =~ <span class="hljs-regexp">/^y/i</span>);
}

<span class="hljs-function"><span class="hljs-keyword">sub</span> <span class="hljs-title">translate</span> </span>{
  <span class="hljs-keyword">my</span> ($text, $lang) = @_;

  <span class="hljs-keyword">my</span> $ua = LWP::UserAgent-&gt;new;
  <span class="hljs-keyword">my</span> $url = <span class="hljs-string">'https://translation.googleapis.com/language/translate/v2'</span>;

  <span class="hljs-keyword">my</span> $res = $ua-&gt;post($url,
		      [
		       <span class="hljs-string">source =&gt;</span> <span class="hljs-string">'en'</span>,
		       <span class="hljs-string">target =&gt;</span> $lang,
		       <span class="hljs-string">format =&gt;</span> <span class="hljs-string">'html'</span>, <span class="hljs-comment"># Could be 'text'</span>
		       <span class="hljs-string">key =&gt;</span> $APIkey,
		       <span class="hljs-string">q =&gt;</span> $text,
		      ]);

  <span class="hljs-keyword">die</span>(<span class="hljs-string">"Failed to access server: "</span>. $res-&gt;status_line . <span class="hljs-string">"\n"</span>)
    <span class="hljs-keyword">unless</span> ($res-&gt;is_success);

  <span class="hljs-keyword">my</span> $data = $res-&gt;content;

  <span class="hljs-keyword">my</span> $json = from_json($data, { <span class="hljs-string">utf8 =&gt;</span> <span class="hljs-number">1</span> } );

  <span class="hljs-keyword">my</span> $translated;

  <span class="hljs-keyword">eval</span> {
    <span class="hljs-keyword">my</span> $d = $json-&gt;{data};
    <span class="hljs-keyword">die</span>(<span class="hljs-string">"Missing \"data\" entry\n"</span>) <span class="hljs-keyword">unless</span> (<span class="hljs-keyword">defined</span> $d);

    <span class="hljs-keyword">my</span> $tr = $d-&gt;{translations};
    <span class="hljs-keyword">die</span>(<span class="hljs-string">"Missing \"translations\" entry\n"</span>)
      <span class="hljs-keyword">unless</span> ((<span class="hljs-keyword">defined</span> $tr) &amp;&amp; (<span class="hljs-keyword">ref</span> $tr eq <span class="hljs-string">'ARRAY'</span>) &amp;&amp;
	     (<span class="hljs-keyword">ref</span> $tr-&gt;[<span class="hljs-number">0</span>] eq <span class="hljs-string">'HASH'</span>));

    $translated = $tr-&gt;[<span class="hljs-number">0</span>]-&gt;{translatedText};

    <span class="hljs-keyword">die</span>(<span class="hljs-string">"No translated text\n"</span>)
      <span class="hljs-keyword">unless</span> (<span class="hljs-keyword">defined</span> $translated);
  };

  <span class="hljs-keyword">die</span>(<span class="hljs-string">"Malformed response from server: $@\n"</span>) <span class="hljs-keyword">if</span> ($@);

  $translated =~ <span class="hljs-regexp">s/(&lt;\/(?:p|h\d+)&gt;)[ \t\n\r]*/"$1\n"/g</span>e;

  <span class="hljs-keyword">return</span> $translated;
}</pre>
<p>The substitution at the end of the translate() function adds a newline after each closing tag for a paragraph or header (e.g. &lt;/p&gt;, &lt;h1&gt; etc.) so that the HTML is more readable with a text editor. Otherwise it&#8217;s all in one single line.</p>
<h3>Protecting your money</h3>
<p>By obtaining an API key, you effectively give your computer permission to spend money. Which is fine as long as it works as intended, but a plain bug in a script that leads to an infinite loop or recursion, or maybe just feeding the system with a huge file by mistake, can end up with consequences that are well beyond the CPU fan spinning a bit.</p>
<p>So there are two protection mechanisms in the script itself:</p>
<ul>
<li>The script prompts for permission, stating how much it will cost (based upon <a rel="noopener" href="https://cloud.google.com/translate/pricing" target="_blank">$20 / million chars</a>).</li>
<li>It limits a single translation to 500k chars (to avoid a huge file from being processed accidentally).</li>
</ul>
<p>Another safety mechanism is to set up budgets and budget alerts. Go to Main menu (hamburger) &gt; Billing &gt; Budgets &amp; Alerts. Be sure to check &#8220;Email alerts to billing admins and users&#8221;. If I got it right, budgets don&#8217;t protect against spending, but only sends notifications. So I selected a sum, and enabled only the 100% threshold. It seems to make sense to check all the Discounts and Promotion options in the Credits part, which makes sure that the alert is given for the money to be spent by deducing all promotion credits.</p>
<p>On top of that, it&#8217;s a good idea to set quota limits: Go to Main menu (hamburger) &gt; IAM &amp; Admin &gt; Quotas. Set the filter to Translation to get rid of a lot of lines.</p>
<p>It&#8217;s also the place to get an accurate figure for the current consumption.</p>
<p>Enable the quota for &#8220;v2 and v3 general model characters per day&#8221;, which is the only character limit that isn&#8217;t per minute, and set it to something sensible, for example 2 million characters if you&#8217;re a modest user like myself. That&#8217;s $40, which is fairly acceptable damage if the computer goes crazy, and high enough not to hit the roof normally.</p>
<p>Also do something with &#8220;v3 batch translation characters using general models per day&#8221; and same with AutoML custom models. I don&#8217;t use these, so I set both to zero. Just to be safe.</p>
<p>There&#8217;s &#8220;Edit Quotas&#8221; to the top right. Which didn&#8217;t work, probably because I did this during the trial period, so quotas are meaningless, and apparently disabled anyhow (or more precisely, enabled to fixed limits).</p>
<p>So the way to do it was somewhat tricky (as it&#8217;s probably pointless): To enable a quota, right-click the &#8220;Cloud Translation API&#8221; to the left of the quota item, and open it in a new tab. Set up the quota figure there. But this description on how to do it might not be accurate for a real-life use. Actually, the system ignored my attempts to impose limits. They appeared on the page for editing them, but not on the main page.</p>
<h3>Supporting CJK in LaTeX</h3>
<p>I&#8217;m wrapping up this post with notes on how to feed LaTeX (pdflatex, more precisely) with Chinese, Japanese and Korean, with UTF-8 encoding, and get a hopefully reasonable result.</p>
<p>So first grab a few packages:</p>
<pre># apt install texlive-lang-european
# apt install texlive-lang-chinese
# apt install texlive-lang-korean
# apt install texlive-cjk-all</pre>
<p>Actually, texlive-lang-european isn&#8217;t related, but as its name implies, it&#8217;s useful for European languages.</p>
<p>I first attempted with</p>
<pre><span class="hljs-keyword">\usepackage</span>[UTF8]{ctex}</pre>
<p>but pdflatex failed miserably with an error saying that the fontset &#8216;fandol&#8217; is unavailable in current mode, <a rel="noopener" href="https://tex.stackexchange.com/questions/545681/critical-package-ctex-errorctex-fontsetfandol-is-unavailable-in-current" target="_blank">whatever that means</a>. After trying a few options back and forth, I eventually went for the rather hacky solution of using CJKutf8. The problem is that CJK chars are allowed only within</p>
<pre><span class="hljs-keyword">\begin</span>{CJK}{UTF8}{gbsn}

<span class="yadayada">[ ... ]</span>

<span class="hljs-keyword">\end</span>{CJK}</pre>
<p>but I want it on the whole document, and I need the language setting to be made in a file that is included by the main LaTeX file (a different included file for each language). So I went for this simple hack:</p>
<pre><span class="hljs-keyword">\AtBeginDocument</span>{<span class="hljs-keyword">\begin</span>{CJK}{UTF8}{gbsn}}
<span class="hljs-keyword">\AtEndDocument</span>{<span class="hljs-keyword">\end</span>{CJK}}</pre>
<p>As for the font, <a rel="noopener" href="https://www.overleaf.com/learn/latex/Chinese" target="_blank">it appears like</a> gbsn or gkai fonts should be used with Simplified Chinese, and bsmi or bkai for with Traditional Chinese. Since I translated into Simplified Chinese, some characters just vanished from the output document when trying bsmi and bkai. The back-translation to English of a document made with bsmi was significantly worse, so these dropped characters had a clear impact in intelligibility of the Chinese text.</p>
<p>I got this LaTeX warning saying</p>
<pre>LaTeX Font Warning: Some font shapes were not available, defaults substituted.</pre>
<p>no matter which of these fonts I chose, so it doesn&#8217;t mean much.</p>
<p>So the choice is between gbsn or gkai, but which one? To decide, I copy-pasted Chinese text from updated Chinese websites, and compared the outcome of LaTeX, based upon the TeX file shown below. It was quite clear that gbsn is closer to the fonts in use in these sites, even though I suspect it&#8217;s a bit of a Times New Roman: The fonts used on the web have less serifs than gbsn. So gbsn it is, even though it would have been nicer with a font with less serifs.</p>
<p>For Japanese, there&#8217;s &#8220;min&#8221;, &#8220;maru&#8221; and &#8220;goth&#8221; fonts. &#8220;Min&#8221; is a serif font, giving it a traditional look (calligraphy style) and judging from Japanese websites, it appears to be used primarily for logos and formal text (the welcoming words of a university&#8217;s president, for example).</p>
<p>&#8220;Maru&#8221; and &#8220;goth&#8221; are based upon simple lines, similar to plain text in Japanese websites. The latter is a bit of a bold version of &#8220;maru&#8221;, but it&#8217;s what seems to be popular. So I went with &#8220;goth&#8221;, which has a clean and simple appearance, similar to the vast majority of Japanese websites, even though the bold of &#8220;goth&#8221; can get a bit messy with densely drawn characters. It&#8217;s just that &#8220;maru&#8221; looks a bit thin compared to what is commonly preferred.</p>
<p>Korean has two fonts in theory, &#8220;mj&#8221; and &#8220;gt&#8221;. &#8220;mj&#8221; is a serif font with an old fashioned look, and &#8220;gt&#8221; is once again the plain, gothic version. I first failed to use the &#8220;gt&#8221; font even though it was clearly installed (there were a lot of files in the same directories as where the &#8220;mj&#8221; files were installed, only with &#8220;gt&#8221;). Nevertheless, trying the &#8220;gt&#8221; font instead of &#8220;mj&#8221; failed with</p>
<pre>LaTeX Font Warning: Font shape `C70/gt/m/it' undefined
(Font)              using `C70/song/m/n' instead on input line 8.

! Undefined control sequence.
try@size@range ...extract@rangefontinfo font@info
                                                  &lt;-*&gt;@nil &lt;@nnil</pre>
<p>But as it turns out, it should be referred to as &#8220;nanumgt&#8221;, e.g.</p>
<pre>\begin{CJK}{UTF8}{<span class="punch">nanumgt</span>}
나는 멋진 글꼴을 원한다
\end{CJK}</pre>
<p>It&#8217;s worth mentioning XeLaTeX, which allows using an arbitrary True Type font withing LaTeX, so the font selection is less limited.</p>
<p>See <a rel="noopener" href="https://tex.my/2010/06/21/cjk-support-in-latex/" target="_blank">this page</a> on fonts in Japanese and Korean.</p>
<p>For these tests, I used the following LaTeX file for use with e.g.</p>
<pre>$ pdflatex test.tex</pre>
<pre><span class="hljs-keyword">\documentclass</span>{hitec}
<span class="hljs-keyword">\usepackage</span>[utf8]{inputenc}
<span class="hljs-keyword">\usepackage</span>[T1]{fontenc}
<span class="hljs-keyword">\usepackage</span>{CJKutf8}
<span class="hljs-keyword">\newcommand</span>{<span class="hljs-keyword">\thetext</span>}
{

它说什么并不重要，重要的是它是如何写的。
}

<span class="hljs-keyword">\AtBeginDocument</span>{}
<span class="hljs-keyword">\AtEndDocument</span>{}
<span class="hljs-keyword">\title</span>{This document}
<span class="hljs-keyword">\begin</span>{document}

gbsn:

<span class="hljs-keyword">\begin</span>{CJK}{UTF8}{gbsn}
<span class="hljs-keyword">\thetext</span>
<span class="hljs-keyword">\end</span>{CJK}

gkai:

<span class="hljs-keyword">\begin</span>{CJK}{UTF8}{gkai}
<span class="hljs-keyword">\thetext</span>
<span class="hljs-keyword">\end</span>{CJK}

bsmi:

<span class="hljs-keyword">\begin</span>{CJK}{UTF8}{bsmi}
<span class="hljs-keyword">\thetext</span>
<span class="hljs-keyword">\end</span>{CJK}

bkai:

<span class="hljs-keyword">\begin</span>{CJK}{UTF8}{bkai}
<span class="hljs-keyword">\thetext</span>
<span class="hljs-keyword">\end</span>{CJK}

<span class="hljs-keyword">\end</span>{document}</pre>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2022/08/google-translate-pdflatex-technical/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
