<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xmlns:tt="http://teletype.in/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Widowan</title><generator>teletype.in</generator><description><![CDATA[Widowan]]></description><image><url>https://img1.teletype.in/files/42/39/4239bdaa-c071-483f-82f2-b78b360aad9f.png</url><title>Widowan</title><link>https://blog.wido.dev/</link></image><link>https://blog.wido.dev/?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=widowan</link><atom:link rel="self" type="application/rss+xml" href="https://teletype.in/rss/widowan?offset=0"></atom:link><atom:link rel="next" type="application/rss+xml" href="https://teletype.in/rss/widowan?offset=10"></atom:link><atom:link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></atom:link><pubDate>Thu, 30 Apr 2026 11:33:33 GMT</pubDate><lastBuildDate>Thu, 30 Apr 2026 11:33:33 GMT</lastBuildDate><item><guid isPermaLink="true">https://blog.wido.dev/wireplumber-scripting</guid><link>https://blog.wido.dev/wireplumber-scripting?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=widowan</link><comments>https://blog.wido.dev/wireplumber-scripting?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=widowan#comments</comments><dc:creator>widowan</dc:creator><title>Wireplumber's lua scripting sucks; here's how to use it</title><pubDate>Fri, 13 Dec 2024 17:01:26 GMT</pubDate><category>IT</category><description><![CDATA[Recently steam rolled out a beta of their game recording feature, but it captures entire system audio on linux. Sounds like a job for wireplumber and pipewire! Oh no, it sucks.]]></description><content:encoded><![CDATA[
  <h2 id="2fHg">The problem</h2>
  <p id="btVc">Recently steam rolled out a beta preview of their game recording feature. It&#x27;s amazing! But, since it&#x27;s a beta, one small problem: on linux, at the time of writing, you can not choose which audio source it&#x27;s recording from, it captures entire system audio. So I wanted to write a relatively simple script to unlink all sinks from steam and link wine64-preloader apps to it instead. Sounds like a job for pipewire!</p>
  <h2 id="6vQz">Gotchas</h2>
  <h3 id="3P8R">Wireplumber doesn&#x27;t have any (good) overview documentation</h3>
  <p id="ZlQG">Here&#x27;s how it works in Pipewire world:</p>
  <ul id="ReNk">
    <li id="6smU">You have nodes that produce/consume sound</li>
    <li id="cjRP">You have ports which regulate settings of how said nodes produce/consume sound</li>
    <li id="nTiF">You have links that connect the ports</li>
  </ul>
  <h3 id="Ac8d">Wireplumber doesn&#x27;t have any (good) scripting documentation</h3>
  <p id="Qg2S">Oh, you want to know all possible types for &#x60;Interest&#x60; object? Too bad.</p>
  <blockquote id="87tP">The type of an interest must be a valid <code>GType</code>, written as a string without the “Wp” prefix and with the first letter optionally being lowercase. The type may match any wireplumber object or interface.</blockquote>
  <p id="77NO">No comprehensive list, go read the possible values from C library.</p>
  <p id="phdt">Oh, you somehow found that wireplumber has eventhooks? Too bad, there&#x27;s only one mention of it in &quot;Wireplumber&#x27;s design&quot; page and no examples whatsoever. Go read source code to find example usage.</p>
  <p id="zv4W">Oh, you want to read examples on github that documentation explicitly mentions? Too bad, they&#x27;re outdated and use legacy api.</p>
  <p id="lqqg">Hell, it doesn&#x27;t even describe all the objects there are! The only circulating snippet around the internet for linking ports has this:</p>
  <pre id="m0Qv" data-lang="lua">local link = Link(&quot;link-factory&quot;, link_args)
link:activate(1)</pre>
  <p id="dble">Link object is not mentioned ANYWHERE in lua scripting documentation. It <em>looks</em> to be a wrapper against <code>wp_link_new_from_factory</code>, but why the hell do I have to guess?</p>
  <p id="zg9x"><strong><em>Awesome</em></strong>, guys. </p>
  <h3 id="G2fc">Wireplumber doesn&#x27;t run &quot;real&quot; lua, it&#x27;s sandboxed context</h3>
  <p id="J0aq">Okay, you found ObjectManager, managed to give it interest and connect it to callback. Great! It works!</p>
  <pre id="85cq" data-lang="lua">local node_om = ObjectManager {
  type = &quot;node&quot;,
  Constraint { &quot;media.class&quot;, &quot;equals&quot;, &quot;Stream/Output/Audio&quot;, type = &quot;pw&quot; }
}

node_om:connect(&quot;object-added&quot;, function(om, object)
  print(&quot;Object exists!&quot;)
end)</pre>
  <p id="2FB5">You run this script via wpexec, and it prints a bunch of stuff! Cool! Now you launch a VLC or something to check that it will print once again and.... it doesn&#x27;t?</p>
  <p id="nle9">Saving you some headache, it&#x27;s because of local. As far as I understand, once it&#x27;s done with first run, wireplumber switches context from script back to the main app and garbage collects all <code>local</code>s (you&#x27;ll also see some &quot;proxy destroyed&quot; errors along the way).</p>
  <p id="SJLV">Do not use locals for ANY ObjectManager objects, global or not.  </p>
  <h3 id="bela">Quick bits</h3>
  <ul id="S8G8">
    <li id="zihE">Wireplumber&#x27;s sandbox doesn&#x27;t expose much if at all from standard lua features. Here&#x27;s comprehensive list from the doc. Don&#x27;t expect to be able to make pid tree here:</li>
  </ul>
  <pre id="6lJE" data-lang="lua">_VERSION  assert       error    ipairs    next       pairs   tonumber
pcall     select       print    tostring  type       xpcall  require
table     string       math     package   utf8       debug   coroutine
os.clock  os.difftime  os.time  os.date   os.getenv</pre>
  <ul id="aRI0">
    <li id="WXrP">It doesn&#x27;t print logs by default (neither wpexec nor wireplumber itself), enable it:<br /><code>WIREPLUMBER_DEBUG=W,&lt;my-logger-name&gt;:T</code></li>
    <li id="Hi6T">You are not guaranteed to have anything, which results on having lots of ObjectManagers and callback to wait for stuff to appear; i.e. do NOT assume that the node that just spawned has ports already, especially if it&#x27;s something like game that takes long time to load</li>
    <li id="vFW7">Use <code>Debug.dump_table(t)</code> to dump properties</li>
  </ul>
  <h2 id="D16V">Okay, how do I actually use it?</h2>
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="xHOL" data-align="center">⚠️ Use qpwgraph app to visually see what&#x27;s happening</p>
  </section>
  <p id="rmJZ">I&#x27;m sorry, I can&#x27;t give you a solution. Your usecase is probably just as non trivial as mine, if you are here.</p>
  <p id="lK7T"><strong>If you&#x27;re still reading, this is your last chance to change your mind and make it in bash via <code>pactl subscribe</code>.</strong></p>
  <p id="K41n">Okay, less words, more examples. Here&#x27;s how I disconnect my steam nodes from every sink input:</p>
  <pre id="vcU5" data-lang="lua">steam_om = ObjectManager { 
  Interest {
	type = &quot;node&quot;,
	Constraint { &quot;application.process.binary&quot;, &quot;matches&quot;, &quot;steam&quot;,              type = &quot;pw&quot; },
	Constraint { &quot;media.class&quot;,                &quot;matches&quot;, &quot;Stream/Input/Audio&quot;, type = &quot;pw&quot; },
  },
}

-- On any steam node, ...
steam_om:connect(&quot;object-added&quot;, function(_, steam_node)
  -- ... We look for links with this node as one it is inputting into,
  steam_link_om = ObjectManager {
    Interest {
      type = &quot;link&quot;,
      Constraint { &quot;link.input.node&quot;, &quot;equals&quot;, steam_node.properties[&quot;object.id&quot;] },
    }
  }

  -- ... Then we look at the Audio/Sink nodes that are sources of this link,
  steam_link_om:connect(&quot;object-added&quot;, function(_, steam_link)
  
    steam_source_om = ObjectManager {
      Interest {
        type = &quot;node&quot;,
        Constraint { &quot;object.id&quot;,   &quot;equals&quot;, steam_link.properties[&quot;link.output.node&quot;] },
        Constraint { &quot;media.class&quot;, &quot;matches&quot;, &quot;Audio/Sink*&quot; },
      }
    }

    -- ... And then we destroy them.
    steam_source_om:connect(&quot;object-added&quot;, function(_, source_node)
    
      local _, err = pcall(function() steam_link:request_destroy() end)
      if err then log:debug(&quot;Destroying error: &quot; .. tostring(err)) end
    
    end)

    steam_source_om:activate()
  end)

  steam_link_om:activate()
end)</pre>
  <p id="ZdkR">And, arguably more hard one, here&#x27;s how to connect every wine64-preload node to steam:</p>
  <pre id="6umz" data-lang="lua">-- For every wine app, ...
wine_om:connect(&quot;object-added&quot;, function(_, wine_node)
  wine_port_om = ObjectManager {
    Interest {
      type = &quot;port&quot;,
      Constraint { &quot;node.id&quot;,        &quot;equals&quot;, wine_node.properties[&quot;object.id&quot;] },
      Constraint { &quot;port.direction&quot;, &quot;equals&quot;, &quot;out&quot; },
    }
  }

  -- ... We&#x27;re waiting for it to have ports (which can very
  -- much be not instant, look at spotify and FFXIV), ...
  wine_port_om:connect(&quot;object-added&quot;, function(_, wine_port)
    -- ..., Comparing them to steam&#x27;s ports, ...
    for steam_node in steam_om:iterate() do
      for steam_port in steam_node:iterate_ports(input_ports) do
      
        -- ..., Checking if those ports match channels, ...
        if steam_port.properties[&quot;audio.channel&quot;] == wine_port.properties[&quot;audio.channel&quot;] then
          -- ... And connect them.
          link_ports(steam_port, wine_port)
        end
      
      end
    end
  end)

  wine_port_om:activate()
end)
</pre>
  <p id="gvJC">And here&#x27;s a function to link two given ports (stolen from <a href="https://bennett.dev/auto-link-pipewire-ports-wireplumber/" target="_blank">here</a>):</p>
  <pre id="c1NE" data-lang="lua">function link_ports(input_port, output_port)
  -- Yes, sometimes I have gotten nil ports, don&#x27;t remove this check
  if not input_port or not output_port then
    log:warning(&quot;nil values, not linking&quot;)
    return
  end

  local link_args = {
    [&quot;link.input.node&quot;]  = input_port.properties[&quot;node.id&quot;],
    [&quot;link.input.port&quot;]  = input_port.properties[&quot;object.id&quot;],
    [&quot;link.output.node&quot;] = output_port.properties[&quot;node.id&quot;],
    [&quot;link.output.port&quot;] = output_port.properties[&quot;object.id&quot;],
    [&quot;object.id&quot;]        = nil,
    [&quot;object.linger&quot;]    = true,
    [&quot;node.description&quot;] = &quot;Link created by steam-wire&quot;,
  }

  local link = Link(&quot;link-factory&quot;, link_args)
  link:activate(1)
end</pre>
  <p id="wTBX">And here is link to full code in case you need it: <a href="https://github.com/Widowan/steam-wire" target="_blank">https://github.com/Widowan/steam-wire</a></p>
  <section style="background-color:hsl(hsl(0, 0%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="8uuf">Put the script into <code>~/.local/share/wireplumber/scripts/</code> (create if not exists) and make <code>~/.config/wireplumber/wireplumber.conf.d/90-steam-wire.conf</code> that looks like this:</p>
    <pre id="JQMC" data-lang="hcl">wireplumber.components = [
  {
    name = steam-wire.lua,
    type = script/lua,
    provides = custom.steam-wire
  }
]

wireplumber.profiles = {
  main = {
    custom.steam-wire = required
  }
}</pre>
  </section>
  <h3 id="dFB1">Really useful resources!</h3>
  <ul id="8Zw8">
    <li id="csAY"><a href="https://bennett.dev/auto-link-pipewire-ports-wireplumber/" target="_blank">https://bennett.dev/auto-link-pipewire-ports-wireplumber/</a> (the goat)</li>
    <li id="vIt2"><a href="https://sourcegraph.com/search?q=context:global+file:.*wireplumber.*+file:.*%5C.conf+script/lua&patternType=keyword&sm=0https://sourcegraph.com/search?q=context:global+file:.*wireplumber.*+file:.*%5C.conf+script/lua&patternType=keyword&sm=0" target="_blank">https://sourcegraph.com/search?q=context:global+file:.*wireplumber.*+file:.*%5C.conf+script/lua&amp;patternType=keyword&amp;sm=0</a></li>
    <li id="ZJKU">Github code search</li>
    <li id="zOHW">I lost more :(</li>
  </ul>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.wido.dev/lxc-wireguard</guid><link>https://blog.wido.dev/lxc-wireguard?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=widowan</link><comments>https://blog.wido.dev/lxc-wireguard?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=widowan#comments</comments><dc:creator>widowan</dc:creator><title>Needlessly complicated: Wireguard inside LXC container as a gateway</title><pubDate>Tue, 24 Oct 2023 09:15:05 GMT</pubDate><media:content medium="image" url="https://img1.teletype.in/files/c1/5b/c15b7778-4285-4d0e-87cd-1ba8ed820920.png"></media:content><category>IT</category><description><![CDATA[<img src="https://img2.teletype.in/files/5c/3f/5c3f279a-dd5f-4b6b-80d4-3f86c4da5ec4.png"></img>It all started with a sudden itch to burn everything to the ground on my VPS server and put everything in LXC containers, including the Wireguard VPN I had. I figured it'd be a good way to learn both LXC and bits of networking, as simple UFW won't do. Well, little did I know it'd also be a major pain for a few days, so here's a short manual to ease it for other people.]]></description><content:encoded><![CDATA[
  <p id="GtUh">It all started with a sudden itch to burn everything to the ground on my VPS server and put everything in LXC containers, including the Wireguard VPN I had. I figured it&#x27;d be a good way to learn both LXC and bits of networking, as simple UFW won&#x27;t do. Well, little did I know it&#x27;d also be a major pain for a few days, so here&#x27;s a short manual to ease it for other people.</p>
  <p id="wHfP">Note that all of the instructions are written for Arch Linux, but since you&#x27;re doing LXC containers, you probably know what to do.</p>
  <h2 id="1han">Installing LXC and LXD</h2>
  <p id="gkVK">First we need to install both LXC and LXD, the first one is the virtualization API itself and the second is it&#x27;s management daemon.</p>
  <pre id="5YvF" data-lang="bash">sudo pacman -S lxc lxd </pre>
  <p id="3vnr">You may also want to consider adding yourself to lxd group to not to do <code>sudo</code> every single time:</p>
  <pre id="n9q4" data-lang="bash">sudo usermod -a -G lxd $USER</pre>
  <p id="ig2T">And speaking of user permissions: to run LXC containers in unprivileged mode, you have to change default mappings of your UID/GIDs:</p>
  <pre id="oS3t" data-lang="bash">sudo usermod -v 1000000-1000999999 -w 1000000-1000999999 root</pre>
  <p id="aLN5">Alternatively you could also edit <code>/etc/subuid</code> and <code>/etc/subguid</code> files directly.</p>
  <p id="i4lh">And don&#x27;t forget to enable the services:</p>
  <pre id="c0im" data-lang="bash">sudo systemctl enable --now lxd lxc</pre>
  <p id="2pJp">Now you should initialize the daemon. You probably want default answers to all of the questions.</p>
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="NGyC" data-align="center"><strong>⚠️ Heads up!</strong></p>
    <p id="JZit">If you get weird errors such as nftables not knowing what masquerade is or any other obscure errors, try to upgrade your kernel! Apparently it&#x27;s really important.</p>
  </section>
  <pre id="ebD1">lxd init</pre>
  <p id="5C9z">That should create everything you need, including <code>lxdbr0</code> interface that will do all the heavy lifting for us on the networking side.</p>
  <p id="Z9Jn">After that, all that&#x27;s left is creating your container (to see all images use <code>lxc image list images:</code>, with colon):</p>
  <pre id="1Dla" data-lang="bash">lxc launch images:archlinux/current/amd64 wireguard</pre>
  <p id="Z19t">Now you can see your container with <code>lxc list</code> and connect to it using <code>lxc exec wireguard bash</code></p>
  <h2 id="Boap">Configuring Wireguard</h2>
  <section style="background-color:hsl(hsl(170, 33%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="4Gsg">Because LXC containers do not virtualize kernel and use host one, we would need to have wireguard kernel module loaded in host, so install <code>wireguard-dkms</code> packge <strong>on the host system </strong>and make sure the module is loaded.</p>
    <p id="Y3gY">However in container we only need userspace package — so you need to install <code>wireguard-tools</code> inside it.</p>
  </section>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="GlDg">Unless you are doing this solely to learn like me, I would recommend using tools such as <a href="https://github.com/gravitl/netmaker" target="_blank">netmaker</a> or at least scripts to manage peers. Unfortunately, beautiful <a href="https://github.com/angristan/wireguard-install" target="_blank">wireguard-install</a> script would require some tinkering to not to refuse to install in LXC, and to do it <em>properly</em> (i.e. not install kernel module).</p>
  </section>
  <p id="vefT">As per disclaimer above, install userspace tools:</p>
  <pre id="m0bJ" data-lang="bash">[root@wireguard ~]$ pacman -S wireguard-tools</pre>
  <p id="Bkno">And configure your wireguard server profile at <code>/etc/wireguard/wg0.conf</code>:</p>
  <pre id="oUWa">[root@wireguard ~]$ wg genkey | tee /etc/wireguard/server-sk.key
[root@wireguard ~]$ cat /etc/wireguard/server-sk.key | wg pubkey | tee /etc/wireguard/server-pk.key
[root@wireguard ~]$ wg genkey | tee /etc/wireguard/peer1-sk.key
[root@wireguard ~]$ cat /etc/wireguard/peer1-sk.key | wg pubkey | tee /etc/wireguard/peer1-pk.key
[root@wireguard ~]$ wg genkey | tee /etc/wireguard/peer2-sk.key
[root@wireguard ~]$ cat /etc/wireguard/peer2-sk.key | wg pubkey | tee /etc/wireguard/peer2-pk.key</pre>
  <pre id="qN8Q" data-lang="toml">[Interface]
Address = 10.10.10.0/24
SaveConfig = true
ListenPort = 51820
PrivateKey = &lt;server-sk goes here&gt;

[Peer]
PublicKey = &lt;peer1-pk goes here&gt;
AllowedIPs = 10.10.10.2/32

[Peer]
PublicKey = &lt;peer2-pk goes here&gt;
AllowedIPs = 10.10.10.3/32</pre>
  <p id="ywRH">Keen eye might notice that we&#x27;re missing the usual masquerade commands in the interface configuration:</p>
  <pre id="MYFl" data-lang="toml">PostUp = iptables -t nat -I POSTROUTING -o eth0 -j MASQUERADE
PreDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE</pre>
  <p id="HU9l">Indeed - it doesn&#x27;t work as is right now, but will start to if we add it. However, the one caveat of that is that <strong>we will not be able to connect to other containers</strong> through VPN by external address: meaning if we, say, have a website hosted at myservice.wido.dev:443, we wouldn&#x27;t be able to connect to it while connected to VPN, even if it&#x27;s public! I have no idea why this is exactly, it may be also because I was using UDP and just missed something specific to it.</p>
  <p id="NXJf">My findings are as follows: if you connect to <code>myservice.wido.dev</code>, for some reason, the request goes from wg0 interface, but never response gets back; despite being masqueraded, the reply arrives designated specifically for eth0 interface and is never de-masqueraded back to wg0. I don&#x27;t know whether this is a bug in LXC/LXD or a mistake of mine that I didn&#x27;t notice, but I&#x27;d be glad to hear anyone who knows.</p>
  <figure id="p1ke" class="m_column" data-caption-align="center">
    <img src="https://img2.teletype.in/files/5c/3f/5c3f279a-dd5f-4b6b-80d4-3f86c4da5ec4.png" width="1240" />
    <figcaption>Here&#x27;s how it looks like</figcaption>
  </figure>
  <p id="ihP3">Okay, so how do you fix it? I&#x27;m gonna save you the two days I&#x27;ve spent looking around and tell you the answer: routing tables! (You can see your ip using <code>lxc list</code>)</p>
  <pre id="vd7y" data-lang="bash">ip route add 10.10.10.0/24 via &lt;ip of your wg container&gt; dev lxdbr0 src &lt;ip of your host machine&gt;
</pre>
  <p id="ZUyD">Don&#x27;t forget to make it permanent (depends on your distro), here&#x27;s the service for systemd-networkd way (see the devices with <code>systemctl list-units</code>):</p>
  <pre id="DnJm" data-lang="toml">[Unit]
Description=&#x27;Add static routes to wireguard IP subnet&#x27;
After=sys-devices-virtual-net-lxdbr0.device
After=lxc.service
After=lxd.service
WantedBy=sys-devices-virtual-net-lxdbr0.device

[Service]
ExecStart=/usr/bin/ip route add 10.10.10.0/24 via &lt;ip of your wg container&gt; dev lxdbr0 src &lt;ip of your host machine&gt;

[Install]
WantedBy=multi-user.target</pre>
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="PrnC" data-align="center"><strong>🚨 (Update) UDP Wild goose chase</strong></p>
    <hr />
    <p id="OHLJ">Quick update: I was banging my head against the wall for an entire week, but I finally figured out why UDP wasn&#x27;t working with this setup, or rather, was working very incosistently: if you send a request to 77.77.77.77, but a reply comes from a different IP address, say, 10.50.50.1 (i.e. lxdbr0&#x27;s address), wireguard will drop such packet. Given that UDP is a stateless protocol, this kind of makes sense, but I just wish it was mentioned anywhere in the internet. Anyways, <strong>this is the reason we have the src part in our routing</strong>.</p>
  </section>
  <p id="30M9">And also, while we&#x27;re at the host system, don&#x27;t forget one more thing: <strong>port forwarding</strong>! Because we don&#x27;t want to do it ourselves, let&#x27;s make LXD do it for us:</p>
  <pre id="dlgh">lxc config device add networking wg-proxy proxy listen=udp:0.0.0.0:51280 connect=udp:127.0.0.1:51280</pre>
  <h2 id="7Hbf">Configuring firewall</h2>
  <p id="DzLX">We&#x27;ve done so much, and it may be working for you already depending on your distro. Congrats! But in reality, we&#x27;re not done yet. One last step we have to do in to configure firewall. I will be using nftables for this, as this setup may imply you have specific networking needs as do I.</p>
  <p id="zEl6">Here&#x27;s how my <code>/etc/nftables/nftables.conf</code> looks like:</p>
  <pre id="uMib" data-lang="graphql">#!/usr/bin/nft -f
# vim:set ts=2 tw=2 sw=2 expandtab:
table inet filter
delete table inet filter

table inet filter {
  chain input {
    # type     = [filter, route, nat]
    # hooks    = [ingress, prerouting, input, forward, output, postrouting]
    # priority = int (mnemonics: mangle = -150, dstnat = -100, filter = 0, srcnat = 100)
    # policy   = [accept, drop]
    type filter hook input priority 0
    policy drop

    # You will NOT get reply back without this
    ct state established,related accept
    ct state invalid             drop
    
    ip protocol icmp accept comment &quot;Allow ICMP&quot;
    iif lo           accept comment &quot;Allow loopback&quot;
    # If you want to configure ACL, you should add a
    # jump-chain here instead of just accept
    iifname &quot;lxdbr0&quot; accept comment &quot;Allow LXC communication&quot;

    tcp dport 3737  accept comment &quot;Allow sshd&quot;
    udp dport 51820 accept comment &quot;Allow wireguard&quot;
    tcp dport 443   accept comment &quot;Allow myservice&quot;

    counter comment &quot;Count input traffic&quot;
  }

  chain forward {
    type filter hook forward priority 0
    policy drop

    oifname &quot;lxdbr0&quot; accept
    iifname &quot;lxdbr0&quot; accept
  }
  
  chain lxc {
    type nat hook postrouting priority srcnat
    policy accept

    oif     &quot;enp1s0&quot; masquerade
    iifname &quot;lxdbr0&quot; masquerade
  }
}</pre>
  <p id="zs2A">With this configuration, you should be done! Congratulations on taming LXC networking.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.wido.dev/cloud-candy</guid><link>https://blog.wido.dev/cloud-candy?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=widowan</link><comments>https://blog.wido.dev/cloud-candy?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=widowan#comments</comments><dc:creator>widowan</dc:creator><title>Облака - не сахарная вата, какой их рекламируют, а неудобный набор велосипедов.</title><pubDate>Fri, 03 Mar 2023 10:17:32 GMT</pubDate><category>IT</category><description><![CDATA[<img src="https://img4.teletype.in/files/fc/c1/fcc1e6b7-e869-4072-9ed1-c194e6db8bef.png"></img>Некоторое время назад отовсюду можно было услышать радостные крики о том, насколько облачные технологии нам всем помогут и упростят разработку. Сегодня я поведаю вам, почему это нихрена не так, как я потратил неделю на написание CRUD'а в облаке и как этого избежать.]]></description><content:encoded><![CDATA[
  <p id="Hstk">Некоторое время назад отовсюду можно было услышать радостные крики о том, насколько облачные технологии нам всем помогут и упростят разработку. Сегодня я поведаю вам, почему это нихрена не так, как я потратил неделю на написание CRUD&#x27;а в облаке и как этого избежать.</p>
  <h2 id="iBeM">Как всё начиналось</h2>
  <p id="cQ56">Одним днём мне захотелось размяться и сделать кое-какой проект. В чем суть проекта — не так важно. На мысли о том что хочется сделать что-нибудь наложилось ещё и то, что я не так давно праздного интереса ради смотрел бесплатные тарифы Яндекс.Облака (Yandex.Cloud, далее YC), и в целом они меня порадовали: бесплатный тир достаточно щедрый и в целом цены не то чтобы очень дорогие (хотя и всё равно дороже AWS даже с текущим курсом). Это особенно важно, учитывая, что мы живём в России 2023 года, где никакие зарубежные сервисы не работают, а среди Российских облачных провайдеров бесплатный тир есть... только у Яндекса ¯\_(ツ)_/¯.</p>
  <p id="pH4y">На самом деле это не <em>самый </em>первый мой опыт работы с Serverless системами — я делал сокращалку ссылок на Cloudflare Workers (<a href="https://wido.dev/evil" target="_blank">wido.dev/evil</a>, например). Но опыт там был на Rust&#x27;е с использованием всего у чего только была приписка Experimental... в общем опыт интересный, но я знал, во что ввязывался (кстати бесплатные тарифы Cloudflare просто фантастически щедрые).</p>
  <p id="5qe7">Суть в том, что я, наслушавшись хвалебных статей о величии и удобстве облаков, ожидал увидеть полноценный mature сервис с SDK и обёртками под все языки и фреймворки. Ох как же я ошибался — их даже для AWS то мало, что уж про Яндекс... Хотя нет, по Яндексу конкретно я ещё проедусь — дальше.</p>
  <h2 id="ok0o">Выясняем разные облака на вкус</h2>
  <p id="yxKD">Итак, вооружаемся словарём хайповых слов которые вы слышали и гуглим таблицу аналогов яндекса:</p>
  <ul id="mdAP">
    <li id="w3oF">AWS Lambda - Yandex Cloud Functions: &quot;Function as a Service&quot; — просто удалённое выполнение функции (на самом деле кода в любом объёме и с любым количеством функций) на серверах. По существу при получении запроса рантайм просто загружает и исполняет вашу функцию. Формулировка так себе, но она важна: существует конкретный <u><strong>рантайм</strong></u> который <strong><u>загружает</u></strong> вашу функцию. Важно это потому, что накладывает ограничения на код: он должен запускаться в определённом рантайме (например только Java 11, не выше) и быть структурирован таким образом, чтобы рантайм смог ваш код загрузить (он не просто выполняет <code>python3 myCode.py)</code></li>
    <li id="B2CK">EC2 - Yandex Compute Cloud: по существу — виртуальные машины (сервера) с возможностью автоматического масштабирования под нагрузку. </li>
  </ul>
  <figure id="fw5z" class="m_custom" data-caption-align="center">
    <img src="https://img4.teletype.in/files/fc/c1/fcc1e6b7-e869-4072-9ed1-c194e6db8bef.png" width="510.971454058876" />
    <figcaption>Так и есть</figcaption>
  </figure>
  <p id="pAMG">Я мог бы долго перечислять аналоги, но эти два мало того что основные, так ещё и прекрасно показывают суть serverless сервисов для тех, кто с ними не знаком: есть обычный облачный (чаще - managed) режим, а есть serverless.</p>
  <p id="HWtg">Оплачиваются такие сервисы по системе Pay-As-You-Go — по фактическому использованию. Однако в случае с managed вариантом... фактическим использованием будет время использования виртуалки! А значит вы по существу просто арендуете ограниченный VPS. Ладно. Будем использовать Serverless - он дешевле.</p>
  <h2 id="HSo7">Yandex Cloud Functions</h2>
  <section style="background-color:hsl(hsl(170, 33%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="OCnp">Сразу сделаю ремарку, что я по стеку — Java разработчик, и всё это решил делать для тренировки работы со Spring Boot, и оценивать буду соответственно с позиции джависта. Однако, думаю, мои мысли применимы ко всем, кроме разработчиков на С++ и Python. И может Go, судя по тому, что я видел в SDK.</p>
  </section>
  <p id="8trb">Итак, открываем YCF и видим следующую картину в плане рантаймов:</p>
  <ul id="PW0L">
    <li id="AGIX">NodeJS (12, 14, 16)</li>
    <li id="ivs6">PHP (7.4, 8.0)</li>
    <li id="1h4T">Python (3.7 - 3.11)</li>
    <li id="7Knq">Golang (1.16 - 1.19)</li>
    <li id="m6Ai">Java (11)</li>
    <li id="VYNi">.NET 3.1</li>
    <li id="AfRJ">R (4.0, 4.2)</li>
    <li id="6eMb">Bash</li>
  </ul>
  <p id="3ulf">Список мягко говоря... не впечатляет, особенно меня, как джависта: пока весь мир уже сидит на 17 джаве, в YC до сих пор 11. Да и остальные рантаймы не то чтобы сильно актуальны.</p>
  <p id="I2H0">Ладно, не 8 и на том спасибо. Как там с этим работать...</p>
  <p id="aoXc"><strong>Вариант 1:</strong> Сделать реализацию стандартного интерфейса Function (или яндексовского YcFunction) и указать её в качестве точки входа. Очевидно что подойдёт это только для совсем простых функций, да и ограничения там тоже уже есть.</p>
  <p id="VFF0"><strong>Вариант 2:</strong> О, есть раздел про Spring Boot! Как раз мой стек! Ну-ка... мда, раздел совсем маленький, и большая его часть — описание ограничений.</p>
  <ul id="Yt9F">
    <li id="R2ps">Не поддерживается Spring Boot Loader (<a href="https://docs.spring.io/spring-boot/docs/3.1.x/reference/htmlsingle/#appendix.executable-jar.alternatives" target="_blank">как его убрать, Appendix E.6</a> или моя <a href="https://gist.github.com/Widowan/3371ab44f764bd471bdac8a0600e7a46" target="_blank">заметка на Gist</a>).</li>
    <li id="wFrD">Функция на Spring Boot не имеет информации о том, какой её эндпоинт был вызван. Для этого надо использовать API Gateway (и соответственно писать OpenAPI спеку, если вы ещё этого не сделали).</li>
    <li id="d7b9">Не поддерживаются некоторые метода HttpServlet* классов.</li>
  </ul>
  <hr />
  <p id="rKjZ">Лааааадно, пофигу. Пишем простой Hello World на спринге, загружаем, настраиваем API Gateway, делаем наконец-то curl запрос и!.. 500 миллисекунд на ответ??? Как-то... ну вообще не впечатляет. Из плюсов — загружать проект можно исходниками, джарником или maven проектом. Удобно.</p>
  <h2 id="jLkd">В начале было слово, и слово это было — докер. Yandex Serverless Containers</h2>
  <p id="g3Fh">Слишком уж не впечатляет производительность и &quot;удобство&quot; Cloud Functions, давайте попробуем контейнеры! Тут нам никто не указ по части рантайма, да и Spring Boot 3.0 официально поддерживает GraalVM Native Image.</p>
  <p id="T8WZ">Для того чтобы использовать контейнеры, нам надо создать реестр в YC (тоже отдельный сервис кстати) и настроить CLI утилиту. Качаем тулзу (есть в AUR), настраиваем, компилируем Native Image (не забывайте -Pnative чтобы собрать минимально возможный и оптимизированный образ, ценой 100% загрузки всех 12 ядер), тегаем как того просит документация, заливаем, делаем curl... 300 мс (и 1.5 секунды на первый холодный запуск).</p>
  <p id="nYgD">Очевидно что контейнеры лучше по всем параметрам, но вот производительность всё равно оставляет желать лучшего. Если что, время запуска на локальной машине у меня — 50 миллисекунд.</p>
  <p id="REQH">Ладно, черт с ним со временем ответа, не критично, пора подключать базу данных.</p>
  <h2 id="xALX">YDB — самый нижний круг ада</h2>
  <p id="V4FC">На этом этапе я ещё даже не подозревал, что меня ждёт...</p>
  <p id="aZtl">Итак, какие варианты serverless баз данных есть у YC? Да никаких, только YDB. Все остальные базы данных, представленные в облаке — managed, т.е. по сути виртуалка с помесячной оплатой, причём стоят они все конских денег — самый нищенский вариант среди всех баз данных начинаются от трёх тысяч рублей в месяц (хотя Clickhouse и Redis минимально можно взять за 2500, а MongoDB минимум 5000).</p>
  <p id="7zTR">Есть YDB, у которой существует serverless вариант с оплатой в подобающем pay as you go формате, а значит мы его и выбираем. Идём в консоль, создаём базу и сразу же сталкиваемся с тем, что таблица можем быть либо в YDB формате (реляционном), либо в документном (AWS DynamoDB-совместимая).</p>
  <h3 id="BCe8" data-align="center">Я совершил фатальную ошибку — я выбрал YDB режим.</h3>
  <p id="uJjD">Хорошо, идём в документацию и видим следующие факты:</p>
  <ul id="wjFm">
    <li id="NJIl">Язык запросов — YQL (очередной велосипед, которые Яндекс очень уж любит переизобретать).</li>
    <li id="fymL">Существует SDK под джаву.</li>
    <li id="MzlZ">Если найти таблицу сравнения SDK, то лучше всех поддерживается Python и C++.</li>
  </ul>
  <p id="Vxds">Ладно, открываем ссылку на SDK для джавы, не теряя времени в ридми находим ссылку на пример использования, открываем и видим две вещи: феерическое качество кода &quot;тестов&quot; и безрадостную картину голого использования JDBC, причем с кастами всего что можно в свои велосипеды (Connection -&gt; YdbConnection, PreparedStatement -&gt; YdbPreparedStatement, ...), что убивает использование даже JdbcTemplate.</p>
  <p id="wjng"><em>Ну не может же у базы данных с JDBC драйвером (пусть даже и таким) не быть библиотеки для работы со Spring Data (JPA)??</em></p>
  <p id="RigT">Шерстим issues и ищем по коду в надежде найти что-то, но ничего. Абсолютно. В стадии &quot;отрицания&quot; отправляемся в гугл и... внезапно находим <em>другой</em> SDK — легаси, на верхушке ридми которого написано про deprecated и дана ссылка на новый SDK, где мы только что были. Но несмотря на то что это &quot;легаси&quot; — этот сдк не брошен, там всё ещё есть коммиты недельной давности.</p>
  <p id="Ykp1">И, что самое главное, тут есть разделы spring-data-jdbc и spring-data-jpa! Радостно открываем их и... видим, что последний коммит в них был 2 года назад и там даже нету джарников и pom (а значит и возможности собрать самостоятельно).</p>
  <p id="QiAe">...</p>
  <p id="FKdF">Ну такого ведь не может быть, да?</p>
  <p id="XEc2">На этом моменте я потратил целый день и перерыл гугл, исходники обоих SDK, все issue, поиск по всему гитхабу (встроенными средствами и через sourcegraph) и могу вам заявить — так и есть. Возможности нормально использовать YDB со спрингом нет. И даже Datasource для спринга создать не получится — драйвера диалекта для Hibernate-то нет, у нас же велосипед, YQL.</p>
  <section style="background-color:hsl(hsl(170, 33%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="rcau">Теоретически может быть возможно указать случайный диалект в Hibernate и использовать Native Query, однако даже так у меня не получилось установить соединение с БД, да и оно намертво отказывалось компилироваться в Native Image, так что я спустя некоторое время просто плюнул на это.</p>
  </section>
  <p id="1W9g">На этом этапе я стал железобетонно уверен, что разработчики Яндекса ненавидят других программистов и людей в целом, и изобретают велосипеды только чтобы мучить всех остальных своими несовместимыми со всеми реализациями.</p>
  <h2 id="xIrW">Помните документный режим, совместимый с DynamoDB? Давайте попробуем его!</h2>
  <p id="XmOc">В этот момент я потерял веру в человечество (и в первую очередь в Яндекс) и решил попробовать документный режим. С ними я никогда раньше не работал (даже с MongoDB), но это меня не остановило. Я удалил старую таблицу и начал создавать новую, документную и...</p>
  <p id="7Saf">Тут всего три типа данных (String, Number и Binary) и есть ключи партицирования и ключи сортировки. Ладно, возможно погуглить всё же придётся.</p>
  <p id="wkVz">Итак, суть для таких же как я:</p>
  <ul id="rnDv">
    <li id="SimL">Вам не нужно больше типов данных, потому что вам нужна 1-2 колонки в админке, собственно под ключи (документные БД не имеют схемы, вы просто загрузите json со всеми нужными колонками).</li>
    <li id="0PBI">В таблице может быть только ключ партицирования, а может ключ партицирования и сортировки. Ключ партицирования — это и есть обычный Primary Key (на самом деле нет).<br />Если есть ещё и ключ сортировки, то PK выступает ключом группировки (т.е. могут быть несколько одинаковых значений ключа партицирования.<br />Грубо говоря — есть таблица песен, где есть исполнители и названия песен. Ключ партицирования — это имя исполнителя, а ключ сортировки — название песни.</li>
    <li id="tZcQ">Не пытайтесь создавать таблицу в которой будет только PK типа Number — это плохая идея, идущая против best practices, да и вас в этом не поддержит SDK: он генерирует случайные UUID (Строковые), а не инкрементальные айдишники. И по-возможности всё таки используйте ключ сортировки — хорошим вариантом всегда является дата (создания, добавления в базу, чего угодно) (официальная <a href="https://www.mongodb.com/blog/post/6-rules-of-thumb-for-mongodb-schema-design" target="_blank">статья mongodb</a> с ориентирами на то, как проектировать БД)</li>
    <li id="LZrJ">Там нет напрямую языка запросов, все действия делаются через API. Однако дальше SDK и библиотеки для спринга этот момент нам абстрагируют.</li>
  </ul>
  <p id="pht8"><strong>Итак, поехали.</strong></p>
  <p id="wvZp">Ставим форк форка бибилотеки для <a href="https://github.com/boostchicken/spring-data-dynamodb" target="_blank">Spring Data</a> (не JPA. Не ставьте стартер JPA, он будет просить драйверы для Hibernate!), делаем всё как в ридми кроме сущности. Если вы сделали простой ключ (только ключ партицирования), то делайте как в ридми; если композитный (партицирование + сортировка), то тут чуть запарнее: видите ли, под капотом там всё равно используются самые обычные аннотации вроде @Entity и @Id, и именно последний ставит палки в колёса: он-то может быть только один, а ключей у нас два...</p>
  <p id="poo1">Поэтому нам надо создать абстракцию, которая будет нашим айдишником. Выглядит это примерно так (с использованием Lombok):</p>
  <pre id="UpQz" data-lang="java">// ShowId.java
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ShowId implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;

    @DynamoDBHashKey
    @DynamoDBAutoGeneratedKey
    private String showId;

    @DynamoDBRangeKey
    @DynamoDBTypeConverted(converter = ZonedDateTimeToStringConverter.class)
    private ZonedDateTime addedAt;
}


// Show.java
@Getter
@Setter
@DynamoDBTable(tableName = &quot;shows&quot;)
public class Show {
    @Id
    // Если тут будут геттеры то SDK
    // подавится и упадёт, даже с @Transient.
    @Getter(AccessLevel.NONE)
    @Setter(AccessLevel.NONE)
    @Transient
    public transient ShowId _showId = new ShowId();

    @DynamoDBHashKey
    private String showId;

    public String getShowId() {
        return _showId.getShowId();
    }

    public void setShowId(String showId) {
        _showId.setShowId(showId);
    }
    
    @DynamoDBRangeKey
    @DynamoDBTypeConverted(converter = ZonedDateTimeToStringConverter.class)
    private ZonedDateTime addedAt;

    public ZonedDateTime getAddedAt() {
        return _showId.getAddedAt();
    }

    public void setAddedAt(ZonedDateTime addedAt) {
        _showId.setAddedAt(addedAt);
    }

    @DynamoDBAttribute
    private String name;

    @DynamoDBAttribute
    private String description;
    
    @DynamoDBAttribute
    private Integer episodes;
}


// ZonedDateTimeToStringConverter.java
public class ZonedDateTimeToStringConverter implements DynamoDBTypeConverter&lt;String, ZonedDateTime&gt; {
    @Override
    public String convert(ZonedDateTime zonedDateTime) {
        return zonedDateTime.toString();
    }

    @Override
    public ZonedDateTime unconvert(String s) {
        return ZonedDateTime.parse(s);
    }
}
</pre>
  <p id="od7H">Да, как можно заметить, SDK амазона до сих пор не умеет конвертировать новые классы даты в строки (а вот старые — умеет).</p>
  <p id="OMsk">Ах да, в конфигурации DynamoDB бин создания БД должен выглядеть как-то так (endpoint так же, как указано в админке яндекса, регион тоже тот, который выбирали (i.e. <code>ru-central1</code>))</p>
  <pre id="OjpD" data-lang="java">@Bean
public AmazonDynamoDB amazonDynamoDB() {
    return AmazonDynamoDBClientBuilder.standard()
        .withCredentials(new AWSStaticCredentialsProvider(
            new BasicAWSCredentials(dynamoDbAccessKey, dynamoDbSecretKey)
        ))
        .withEndpointConfiguration(
            new AwsClientBuilder.EndpointConfiguration(dynamoDbEndpoint, dynamoDbRegion))
        .build();
}</pre>
  <p id="87Yb">Кстати, видите Access Key и Secret Key? В админке YC это называется &quot;Статический ключ доступа&quot;. А говорю я это потому, что в YC <strong><u>ШЕСТЬ СПОСОБОВ АВТОРИЗАЦИИ!</u></strong> Я сразу оставлю <a href="https://cloud.yandex.ru/docs/iam/concepts/authorization/" target="_blank">ссылку</a> на документацию, она вам понадобится. Помните про велосипеды? Вот то-то и оно...</p>
  <hr />
  <p id="M0C7">На самом деле, использования DynamoDB был значительно менее болезненным, однако я всё равно в шоке, что SDK амазона тоже ничего не умеет. Казалось бы, AWS — это с огромным отрывом лидер в облачных технологиях, но нет, если бы не коммьюнити — пришлось бы писать на встроенном маппере... чуть лучше JDBC, но приятного мало.</p>
  <h2 id="2Sxg">Вместо вывода</h2>
  <p id="jowi">На то чтобы написать простой CRUD ушло просто непозволительно много времени, пусть и частично из-за моего упёрства и перфекционизма, но мы что, варвары, писать на JDBC без причины в 2023 году?</p>
  <p id="hFoS">Я не хочу сказать, что Яндекс Облако — плохой сервис, как раз наоброт: это удобный сервис, и единственный, который работает в т.ч. по формату B2C, потому что все остальные serverless решения в России работают почти исключительно в формате B2B.</p>
  <p id="9KJM">Тем не менее, желание Яндекса переизобретать велосипеды, которые до него изобретали уже десятки корпораций, которые уже устоялись и стандартизировались, накорню рубит удобство использования их продуктов, и это очень печально. Яндекс запустил YDB в облаке уже давно (а год назад его ещё и открыл в опенсорс), однако не позаботился о написании нормального SDK, как это сделали амазон — их SDK тоже не фонтан, но это всё равно не идёт ни в какое сравнение с тем, что на общее обозрение выставил яндекс.</p>
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="gVEE" data-align="center">Не изобретайте велосипеды, пожалуйста. Придерживайтесь стандартов индустрии.</p>
  </section>
  <p id="6rJh">Стандарты позволяют чуть-чуть упростить разработку, потому что сейчас:</p>
  <ul id="D7Gu">
    <li id="Dg63">Разработка под облака — это больно.</li>
    <li id="y6EJ">Деплой под облака — это больно.</li>
    <li id="Jhfz">Интеграция с облаками — это больно.</li>
    <li id="YypS">Вообще любое соприкосновение с облаками — это больно.</li>
  </ul>
  <p id="k0J6">Давайте сделаем их чуточку удобнее, ладно?</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.wido.dev/domain-only-infrastructure</guid><link>https://blog.wido.dev/domain-only-infrastructure?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=widowan</link><comments>https://blog.wido.dev/domain-only-infrastructure?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=widowan#comments</comments><dc:creator>widowan</dc:creator><title>Обживаемся в интернете с одним только доменом не платя ни копейки за хостинг</title><pubDate>Wed, 30 Nov 2022 14:12:34 GMT</pubDate><media:content medium="image" url="https://img4.teletype.in/files/f5/68/f568ac2f-0430-4f78-bb08-36a6266bb747.png"></media:content><description><![CDATA[<img src="https://img1.teletype.in/files/86/94/86941a29-a1c9-4485-8126-375b872aa18f.png"></img>Возможно вам захотелось свой сайт. Или блог. Или просто почту на своём домене, чтобы красиво. А возможно вас пугает перспектива мигрировать все свои аккаунты на новую почту. Не суть важно — поехали делать комбайн с одним только доменом без хостинга!]]></description><content:encoded><![CDATA[
  <p id="LcO8">Возможно вам захотелось свой сайт. Или блог. Или просто почту на своём домене, чтобы красиво. А возможно вас пугает перспектива мигрировать все свои аккаунты на новую почту. Не суть важно — поехали делать комбайн с одним только доменом без хостинга!</p>
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="p99p">Примечание: эта заметка не имеет ультра подробных инструкций и в целом рассчитана на людей, которые знают, в чём разница между git и GitHub и как примерно работает DNS.</p>
  </section>
  <nav>
    <ul>
      <li class="m_level_1"><a href="#d6qd">Обзаводимся доменом</a></li>
      <li class="m_level_2"><a href="#YN1f">Получаем домен бесплатно</a></li>
      <li class="m_level_1"><a href="#WmI8">Переносим домен на DNS Cloudflare</a></li>
      <li class="m_level_1"><a href="#AiD7">Настраиваем почту</a></li>
      <li class="m_level_2"><a href="#kezr">Настройка получения писем (inbound)</a></li>
      <li class="m_level_2"><a href="#Y2I7">Настройка отправки писем (outbound)</a></li>
      <li class="m_level_1"><a href="#clG6">Настраиваем сайт</a></li>
      <li class="m_level_2"><a href="#sUGt">Выбор темы для Jekyll</a></li>
      <li class="m_level_2"><a href="#Y5BR">Развёртывание и настройка темы</a></li>
      <li class="m_level_2"><a href="#l8Fe">Публикация сайта</a></li>
      <li class="m_level_2"><a href="#2rB1">Если вам нужен только блог на главной странице без возни с Jekyll</a></li>
      <li class="m_level_1"><a href="#msBt">Бонус: верификация домена на GitHub</a></li>
      <li class="m_level_1"><a href="#5esy">Бонус: итоговый вид DNS таблицы</a></li>
      <li class="m_level_1"><a href="#DOmD">Домашнее задание: облака и serverless</a></li>
    </ul>
  </nav>
  <h2 id="d6qd">Обзаводимся доменом</h2>
  <p id="Fbms">Домен можно либо купить за 150-1500 рублей в год (смотря в какой зоне; .ru самые дешёвые), либо получить бесплатно — в зонах .tk, .ml, .ga, .cf, и .gq (конечно, со звёздочкой).</p>
  <section style="background-color:hsl(hsl(170, 33%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="ayn8">Если вы собираетесь покупать домен за деньги, то советую обратить внимание на менее популярных регистраторов вроде <a href="http://fozzy.ru" target="_blank">Fozzy</a>, <a href="https://timeweb.com" target="_blank">Timeweb</a>, <a href="https://beget.com" target="_blank">Beget</a> и других — у них обычно более низкие и прозрачные цены и лучше поддержка.</p>
  </section>
  <h3 id="YN1f">Получаем домен бесплатно</h3>
  <p id="vQlO">Бесплатные доменные имена в зонах .tk, .ml, .ga, .cf, и .gq выдаёт регистратор <a href="https://freenom.com/" target="_blank">Freenom</a>. Но конечно всем нам очевидно, что бесплатный сыр только в мышеловке, за сами следующие предупреждения:</p>
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="VhBN">Freenom, согласно пользовательскому соглашению и отзывам, может сделать продление вашего домена платным, если он станет достаточно популярным. Не критично для личных сайтов визиток, но стоит держать это в уме — регистратор очень сомнительного качества (зато бесплатно).</p>
  </section>
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="H0fu">Бесплатные домены привлекают спамеров, поэтому настоятельно не рекомендую регистрировать такой, если вам хочется свою собственную почту: письма с таких адресов очень часто попадают в спам или напрямую блокируются администраторами, поскольку <a href="https://www.spamhaus.org/statistics/tlds/" target="_blank">являются лидерами</a> по количеству спама.</p>
  </section>
  <h2 id="WmI8">Переносим домен на DNS Cloudflare</h2>
  <p id="23rl">После этого стоит перенести DNS хостинг домена к Cloudflare, поскольку а) мы воспользуемся сервисом Cloudflare для настройки почты, б) он объективно один из лучших.</p>
  <p id="qLiD">Для этого необходимо сделать следующее:</p>
  <ol id="hHOy">
    <li id="jwoG">Зарегистрироваться на Cloudflare.com</li>
    <li id="WyxR">В разделе Websites добавить свой сайт</li>
    <li id="ymz3">После этого вам выдадут два адреса, которые нужно прописать в раздел nameservers у регистратора вашего домена вместо стандартных.</li>
  </ol>
  <section style="background-color:hsl(hsl(170, 33%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="UtPJ">Обновление именных серверов вашего сайта обычно занимает до нескольких часов (хотя иногда всего пару минут).</p>
    <p id="ygTT">Проверить текущий статус можно командой <code>dig</code> (или похожими веб сервисами): <code>dig example.com NS +noall +answer</code></p>
    <p id="AdNw">Пока вы ждёте переноса — можете ознакомиться сервисами, которые есть в бесплатном тарифе Cloudflare. Чем сильнее вам нравится делать разные крутые штуки в программировании и около, тем сильнее у вас загорятся глаза :)</p>
  </section>
  <h2 id="AiD7">Настраиваем почту</h2>
  <h3 id="kezr">Настройка <strong>получения писем (inbound)</strong></h3>
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="bm4B">Настраивать почту мы будет пересылкой на другой ящик, поэтому вам не придётся менять уже и так удобный вам интерфейс и сервис, и переносить адрес вы сможете плавно; но также это означает, что вам нужно найти устраивающего вас провайдера почты.</p>
  </section>
  <p id="fYTl">Для пересылки писем на наш адрес мы будем использовать Cloudflare:</p>
  <ol id="QlGo">
    <li id="2Aq6">Открываем наш сайт в личном кабинете cloudflare</li>
    <li id="CBDu">Выбираем раздел Email → Email Routing</li>
    <li id="9HZ2">Cloudflare предложит нам автоматически добавить все необходимые DNS записи. Соглашаемся.</li>
    <li id="VWKF">Выбираем тип пересылки, который нам нужен:</li>
    <ol id="QbfH">
      <li id="9vOf"><strong>Кастомные адреса:</strong> пересылка только с конкретных адресов, которые вы настроите, например <code>contact@example.com</code> или <code>admin@example.com</code>, остальные игнорируются.</li>
      <li id="y58C"><strong>Catch-all:</strong> пересылаются любые адреса, например <code>asdfgh@example.com</code> или <code>github@example.com</code> (рекомендую именно этот тип, вы сможете регистрироваться на любых сайтах адресом с названием сайта).</li>
    </ol>
    <li id="tErW">Выбираем адрес куда пересылается письмо.</li>
    <li id="H1JJ">Всё, вы восхитительны!</li>
  </ol>
  <section style="background-color:hsl(hsl(170, 33%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="TqCT">Хорошей альтернативой пересылке от Cloudflare может стать сервис forwardemail.net, но в нём пересылка настраивается через DNS TXT запись вида <code>forward-email=user@gmail.com</code>, что раскрывает целевой ящик кому угодно, кто решит проверить DNS записи вашего домена: (. В остальном сервис отличный!</p>
  </section>
  <hr />
  <p id="IuY4"><strong>Настройка почтового клиента на входящие письма</strong> (если вы пользуетесь отдельным приложением, а не вашей почтой в браузере): согласно вашему провайдеру почты. Искренне сочувствую пользователям Protonmail и Tutanota, где не поддерживается IMAP (то есть самому себе).</p>
  <hr />
  <h3 id="Y2I7">Настройка отправки писем (outbound)</h3>
  <p id="n2rY">С отправкой всё несколько сложнее, потому что получать репутацию спам сервиса никто не хочет, а спамеры будут использовать любые возможности, поэтому этим мало кто занимается. Согласно <a href="https://www.statista.com/statistics/420400/spam-email-traffic-share-annual/" target="_blank">статистике</a>, 40% всего трафика электронной почты это спам (а раньше было 80%).</p>
  <p id="D0j1">С отправкой писем нам поможет сервис <a href="https://www.smtp2go.com/" target="_blank">SMTP2GO</a>. В бесплатном тарифе он предлагает 1000 писем в месяц, чего для личного пользования хватит с головой. <strong>Сервис хранит отправленные письма в течение пяти дней.</strong></p>
  <p id="5SQ8">Всё достаточно тривиально:</p>
  <ol id="zhaP">
    <li id="5CjE">Регистрируемся и заходим в личный кабинет</li>
    <li id="MRjV">Добавляем адрес сайта</li>
    <li id="k5zt">Подтверждаем добавлением указанных DNS записей в админке Cloudflare</li>
    <li id="UMmX">Создаем нового пользователя SMTP в разделе <em>SMTP Users</em>.</li>
  </ol>
  <section style="background-color:hsl(hsl(199, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="DPs5">При добавлении DNS записей в профиле smtp2go можно изменить имена поддоменов, например на smtp.example.com, etc.</p>
  </section>
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="urto">Вместо использования стандартных SPF/DKIM/DMARC проверок, <a href="https://www.smtp2go.com/blog/spf-record/" target="_blank">SMTP2GO использует</a> технику <a href="https://en.wikipedia.org/wiki/Variable_envelope_return_path" target="_blank">VERP</a>, поэтому вам не нужно обновлять SPF записи и т. д., вам нужна только CNAME запись на return.smtp2go.com, которую вы уже добавили выше.</p>
  </section>
  <section style="background-color:hsl(hsl(170, 33%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="pvsu">Я также пробовал зарегистрироваться в сервисе Sendgrid, однако он требует полной верификации для использования (ФИО + Телефон) и не рассчитан для личного использования — он также требует подтверждения компании через соцсети или иным способом.</p>
  </section>
  <hr />
  <p id="5m3m"><strong>Настройка почтового клиента на исходящие письма (для всех):</strong> <a href="https://www.smtp2go.com/setupguide/" target="_blank">тут для любого софта</a> / <a href="https://www.smtp2go.com/setupguide/thunderbird/" target="_blank">конкретно для Thunderbird</a>.</p>
  <p id="isGp">Всё, отправляйте тестовое письмо! </p>
  <hr />
  <h2 id="clG6">Настраиваем сайт</h2>
  <p id="8x3i">Если вам нужен статический сайт (визитка, блог, etc), то проще всего использовать Jekyll и GitHub Pages, что и будет описано дальше.</p>
  <p id="VUn0">Блог, например, можно сделать с помощью всё того же Jekyll (если тема такое поддерживает) или Hugo, либо с помощью сервисов вроде Teletype (где вы и читаете эту статью).</p>
  <p id="AFGE">Я выбрал второе из-за WYSIWYG редактора и отсутствия необходимости каждый раз добавлять статьи в качестве Markdown файлов на гитхаб и встроенных комментариев.</p>
  <section style="background-color:hsl(hsl(170, 33%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="vXpj">Хочу обратить ваше внимание, что существует достаточно большое количество сервисов, которые позволяют встраивать динамические элементы со своих серверов, например Commento.io для комментариев или полноценных Headless CMS систем.</p>
  </section>
  <h3 id="sUGt">Выбор темы для Jekyll</h3>
  <p id="9swD">Для начала необходимо выбрать тему для Jekyll. Традиционно GitHub Pages поддерживал только минимальное количество тем, которые ещё и плохо настраивались.</p>
  <p id="k5sb">Но с недавних пор там появилась поддержка развёртывания сайтов с помощью GitHub Actions, из-за сайты теперь можно делать на чём угодно, а для Jekyll выбирать какие угодно темы. Собственно подобрать их можно, например, тут:</p>
  <ul id="Z2mY">
    <li id="2kWe"><a href="https://jamstackthemes.dev" target="_blank">jamstackthemes.dev</a></li>
    <li id="xgO3"><a href="http://jekyll-themes.com/free/" target="_blank">jekyll-themes.com</a></li>
    <li id="Tsi3"><a href="http://jekyllthemes.org" target="_blank">jekyllthemes.org</a></li>
  </ul>
  <p id="Zw3s">На моём сайте — <a href="https://wido.dev" target="_blank">wido.dev</a> — стоит тема <a href="https://github.com/BDHU/minimalist/" target="_blank">Minimalist</a>.</p>
  <h3 id="Y5BR">Развёртывание и настройка темы</h3>
  <p id="xBIl">С настройкой темы у всех всё индивидуально, поэтому обращайтесь к документации выбранной темы. А вот развёртывание у всех примерно одинаковое: </p>
  <ol id="wLmr">
    <li id="J7rg">Ставим Ruby и Bundle (если он у вас не идёт вместе с Ruby)</li>
    <li id="qnT3">Скачиваем нужную тему, переходим в её директорию</li>
    <li id="S4dq"><code>bundle config set --local path &quot;vendor/bundle&quot;</code></li>
    <li id="48vg"><code>bundle install</code></li>
    <li id="Qm6d"><code>bundle exec jekyll serve</code> — учтите, что он автоматически перезагружает контент сайта, но не <code>_config.yml</code></li>
    <li id="BkIm">Играйтесь, настраивайте</li>
  </ol>
  <h3 id="l8Fe">Публикация сайта</h3>
  <p id="80F5">Для публикации сайта на GitHub Pages вам необходимо:</p>
  <ol id="c6vu">
    <li id="zSH5">Создать репозиторий для сайта (если у вас есть GitHub Pro — можно приватный, иначе — публичный) и залить туда результаты вашей настройки</li>
    <li id="dtkc">Перейти в настройки → Pages и в разделе Source выбрать GitHub Actions</li>
    <li id="ovfn">Выбрать Jekyll (<strong>НЕ «GitHub Pages Jekyll»</strong>) среди списка всех Workflow</li>
    <li id="Tiwv">В разделе <code>Jobs -&gt; build -&gt; &quot;Build with Jekyll&quot;</code> YAML файла, который нам предложит отредактировать GitHub, убрать <code>--baseurl &quot;${{ steps.pages.outputs.base_path }}&quot;</code><br />То есть должно остаться только: <code>run: bundle exec jekyll build</code><br />(этот шаг нужен чтобы разместить сайт на корне вашего домена, а не example.com/repositoryname)</li>
    <li id="yDHv">Вернуться в настройки Pages и прописать свой домен (можно поставить галочку Enforce HTTPS)</li>
    <li id="ZXO4">Войти в Cloudflare и прописать следующие настройки DNS (на всякий случай актуальные адреса <a href="https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site#configuring-an-apex-domain" target="_blank">тут</a>), <strong>не забудьте снять галочку Proxy</strong>:</li>
  </ol>
  <pre id="DNXF">Тип записи | Имя | Адрес
A             @    185.199.108.153
A             @    185.199.109.153
A             @    185.199.110.153
A             @    185.199.111.153
AAAA          @    2606:50c0:8000::153
AAAA          @    2606:50c0:8001::153
AAAA          @    2606:50c0:8002::153
AAAA          @    2606:50c0:8003::153
CNAME        www   your-username.github.io</pre>
  <p id="vBOP">Всё, ждите обновления DNS записей.</p>
  <hr />
  <h3 id="2rB1">Если вам нужен только блог на главной странице без возни с Jekyll</h3>
  <p id="WtfU">В случае Teletype это делается достаточно просто: создаём блог, в его настройках указываем ваш домен и нужную DNS CNAME запись:</p>
  <ul id="Rl2n">
    <li id="cRUC">Если вам нужно, чтобы ваш блог отображался на главной странице:<br /><code>CNAME @ domains.teletype.in</code></li>
    <li id="PIfz">Если по поддомену (например blog.example.com):<br /><code>CNAME blog domains.teletype.in</code></li>
  </ul>
  <p id="j7Kp">И нажимаем кнопочку подтверждения после обновления. Всё!</p>
  <section style="background-color:hsl(hsl(199, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="RTB9">Из альтернатив есть ещё <a href="https://hashnode.com/" target="_blank">Hashnode</a>, но мне не нравится насколько он агрессивно рекламирует себя (как сервис) и как он выглядит. На эту тему даже есть статья: <a href="https://coffee-web.ru/blog/free-hashnode-custom-domains-are-designed-to-do-one-thing-advertise-hashnode/" target="_blank">«Бесплатные» пользовательские домены Hashnode предназначены для одной цели: рекламы Hashnode</a></p>
  </section>
  <h2 id="msBt">Бонус: верификация домена на GitHub</h2>
  <p id="LDeE">Зайдите в настройки своего профиля GitHub, раздел Pages, укажите адрес своего сайта и добавьте указанную DNS TXT запись в Cloudflare. Подождите обновления, нажмите кнопочку Verify.</p>
  <section style="background-color:hsl(hsl(170, 33%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="t4NR">Проверить состояние DNS записи можно так:</p>
    <p id="6MKD"><code>dig _github-pages-challenge-YourUsername.example.com +noall +answer TXT</code></p>
  </section>
  <h2 id="5esy">Бонус: итоговый вид DNS таблицы</h2>
  <figure id="OQr8" class="m_original" data-caption-align="center">
    <img src="https://img1.teletype.in/files/86/94/86941a29-a1c9-4485-8126-375b872aa18f.png" width="875" />
    <figcaption>Итоговая таблица в админ-панели Cloudflare</figcaption>
  </figure>
  <h2 id="DOmD">Домашнее задание: облака и serverless</h2>
  <p id="e6B0">Если вы хотите сделать что-то динамическое, то советую глянуть на Yandex Cloud и Cloudflare R2. У первого очень неплохие бесплатные лимиты на Serverless вычисления (Yandex Cloud Function), а R2 предоставляет чудовищные <strong>10 гигабайт</strong> хранилища в месяц бесплатно! </p>
  <p id="0fVq">Это уже совершенно другая тема, но потенциал, как видите, огромный :)</p>
  <p id="UwOj">Если вам нужно что-то попроще, то можно использовать <a href="https://pipedream.com" target="_blank">pipedream.com</a> — это IFTTT на стероидах с вебхуками.</p>
  <p id="RgAR">Удачи!</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.wido.dev/Review-The-Fruit-of-Grisaia</guid><link>https://blog.wido.dev/Review-The-Fruit-of-Grisaia?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=widowan</link><comments>https://blog.wido.dev/Review-The-Fruit-of-Grisaia?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=widowan#comments</comments><dc:creator>widowan</dc:creator><title>The Fruit of Grisaia — обзор визуальной новеллы</title><pubDate>Sat, 26 Nov 2022 08:13:40 GMT</pubDate><description><![CDATA[<img src="https://img1.teletype.in/files/c3/4e/c34e486c-d31c-42fc-aabb-7fcb3f08d038.jpeg"></img>Очень сложно подвести какую-то черту после новелл подобного масштаба. Всё таки не моэге на один вечер — согласно VNDB её длительность без малого 75 часов, и это только первая часть, а ведь есть ещё два сиквела (на самом деле нет, но об этом в следующий раз). Тем не менее, пусть и последний раз я писал развёрнутые отзывы очень давно и не в таких масштабах, я попробую. Обзор содержит небольшие спойлеры к первой паре часов игры, ибо без них тяжко.]]></description><content:encoded><![CDATA[
  <p id="OmdO">Очень сложно подвести какую-то черту после новелл подобного масштаба. Всё таки не моэге на один вечер — согласно <a href="https://vndb.org/v5154" target="_blank">VNDB</a> её длительность без малого 75 часов, и это только первая часть, а ведь есть ещё два сиквела (на самом деле нет, но об этом в следующий раз). Тем не менее, пусть и последний раз я писал развёрнутые отзывы очень давно и не в таких масштабах, я попробую. Обзор содержит небольшие спойлеры к первой паре часов игры, ибо без них тяжко.</p>
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="h3fh">Если вы откроете страничку игры на VNDB — я настоятельно советую выключить даже minor spoilers!</p>
  </section>
  <figure id="UZMV" class="m_retina">
    <img src="https://img1.teletype.in/files/c3/4e/c34e486c-d31c-42fc-aabb-7fcb3f08d038.jpeg" width="500" />
  </figure>
  <h2 id="6Neo">Синопсис</h2>
  <p id="dEYg">Главный герой, Казами Юджи, не старше двадцати лет отроду, работает на секретную правительственную организацию, которая занимается &quot;устранением&quot; неугодных государству людей, однако к моменту начала новеллы он решает подать в отставку и перевести в обычные школу в поисках нормальной жизни. Однако человеку военному не то чтобы есть место среди обычных студентов, поэтому и поступает он по знакомству в специальную школу &quot;для детей с особыми обстоятельствами&quot;. Во всей школе всего пять студентов, и все - главные героини, конечно же.</p>
  <hr />
  <p id="t5xp">Тут стоит отметить, что новелла вышла в 2011 году, и пусть местами это заметно, но в общем и целом — она ничуть не устарела <s>(кроме разрешения 576p)</s>, ведь уже на момент выхода деконструировала свои клише и из-за этого до сих пор выглядит свежо. Поэтому не дайте касту вас отпугнуть:</p>
  <h2 id="zFIw">Каст</h2>
  <p id="jbez">Итак, у нас есть:</p>
  <figure id="K8lh" class="m_custom" data-caption-align="center">
    <img src="https://s2.vndb.org/ch/63/76363.jpg" width="250" />
    <figcaption>Ирису Макина</figcaption>
  </figure>
  <p id="3E1m">Ирису Макина — неугомонная и гиперактивная лоля, которая боится окружающих и ищет заботу и внимание в людях вокруг себя. Иногда может вести себя глуповато, хотя и понятно, что она скорее просто не хочет взрослеть.</p>
  <figure id="i2xO" class="m_custom" data-caption-align="center">
    <img src="https://s2.vndb.org/ch/64/76364.jpg" width="250" />
    <figcaption>Комине Сачи</figcaption>
  </figure>
  <p id="TFDT">Комине Сачи — вечно ходящая в униформе горничной староста класса и (почти) всегда серьёзная, выполнит любой (<em>вообще любой</em>) приказ как данное, даже не задумываясь, из-за чего является опорой всех остальных студентов, которые уже разучились жить без её помощи.</p>
  <figure id="psPr" class="m_custom" data-caption-align="center">
    <img src="https://s2.vndb.org/ch/65/76365.jpg" width="250" />
    <figcaption>Матсушима Мичиру</figcaption>
  </figure>
  <p id="qlh1">Матсушима Мичиру — перекрашенная блондинка с хвостиками, которая очень-очень упорно пытается играть роль цундере, однако получается только роль шута. Может быть на удивление тактичной и читать атмосферу, пытаясь разрядить её своим шутовством.</p>
  <figure id="9Bb0" class="m_custom" data-caption-align="center">
    <img src="https://s2.vndb.org/ch/66/76366.jpg" width="250" />
    <figcaption>Сакаки Юмико</figcaption>
  </figure>
  <p id="CwI5">Сакаки Юмико — дочь владельца академии (и по совместительству первый студент) с проблемами с социализацией. Постоянно скрывает свои эмоции берясь за канцелярский нож и кидаясь (не по-настоящему) на всех подряд, в том числе встречая так главного героя после первой попытки заговорить.</p>
  <figure id="vA2Y" class="m_custom" data-caption-align="center">
    <img src="https://s2.vndb.org/ch/67/76367.jpg" width="250" />
    <figcaption>Суо Амане</figcaption>
  </figure>
  <p id="sM4F">Суо Амане — очень высокая девушка, которая играет роль старшей сестры (или даже матери) для всех в общежитии, заботится о Макине и сразу проявляет интерес к главному герою, не стесняясь бравировать своим телом, за что давно получила много прозвищ.</p>
  <hr />
  <h2 id="KtQ1">О чем игра на самом деле</h2>
  <p id="N9Vl">Вы прочитали описание каста выше? Заметили, что оно какое-то маленькое и невзрачное? На это есть причина — вы можете забыть сразу же всё то, о чем я написал выше. Нет, вся информация там как бы легитимна, всё так, однако это очень однобокое описание персонажей, и даже если вы прочитаете чуть более полную версию на VNDB — ситуацию это не изменит. </p>
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="QdZm">На самом деле вся игра — она про персонажей, их личности и прошлое.</p>
  </section>
  <p id="AKGL">Помните в синопсисе фразу &quot;школа для людей с особенными обстоятельствами&quot;? Вот этим обстоятельствам на самом деле и посвящена вся игра, и пусть в ней нет претензии на философичность или истинное познание жизни, сценаристы явно пытались вложить в неё очень многое. </p>
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="pErJ">Персонажи в игре — очень, очень комплексные и описать их одним или даже несколькими абзацами невозможно.</p>
  </section>
  <p id="snLc">В самой игре есть метафора о характерах людей, по памяти — примерно такая:</p>
  <blockquote id="35DC">Люди похожи на геометрические фигуры. Изначально их личность похожа на куб, но чем больше они сталкиваются с другими людьми, проблемами и обществом, тем больше их углы тупятся скругляются, в идеале в конце превращаясь в шар. Однако, если столкновение будет слишком сильным — это может оставить непоправимые вмятины на личности человека.</blockquote>
  <p id="FDdf">Я не уверен, насколько эта мысль применима к реальности, однако она очень хорошая применима к самой игре:</p>
  <section style="background-color:hsl(hsl(199, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="7oQi">У всех персонажей очень тяжёлое прошлое и огромные проблемы, и в первую очередь это касается протагониста.</p>
  </section>
  <p id="APWj">Многие люди уже привыкли к тому, что протагонист в новеллах является пустышкой для ассоциации игрока с ним и своего рода <a href="https://ru.wikipedia.org/wiki/%D0%9C%D1%8D%D1%80%D0%B8_%D0%A1%D1%8C%D1%8E" target="_blank">Мэри Сью</a>, однако в данном случае Юджи — самый главный персонаж всей истории. Его прошлое и характер раскрывается нам по крупицам в каждом руте, и мы только к концу первого рута будем иметь относительно полное представление о том, что он за человек.</p>
  <h3 id="kOKc">Повествование</h3>
  <p id="Wz0c">Юджи, как было уже сказано, очень необычный для визуальных новелл протагонист. У него есть свой характер и личность, но при этом он не похож на эксцентричного Нариту Шинри из «Hello Lady», вся личность Юджи со всеми недостатками — это груз прошлого и его психологических проблем, в этой истории он учится социализироваться, как и все остальные героини. Это же, кстати, убирает вопрос &quot;а почему это несколько девушек влюбились в обычного ОЯШа?&quot; который может возникать в большинстве гаремников — всем героиням тут нехватает общества и внимания, а студентов парней до Юджи тут не было в принципе.</p>
  <p id="BKdZ">Сложно назвать Юджи &quot;гигачадом&quot; как например упомянутого Нариту, но в сравнении с ним, персонажу Юджи, как ни странно, сильнее веришь, и симпатии он вызывает больше. Тем не менее, мы смотрим на историю почти исключительно от его лица, со всеми вытекающими. Он не твердолобый, скорее просто апатичный и холодный, и именно из-за этого он и поступил в эту академию.</p>
  <h3 id="Ef7P">Жанр повествования</h3>
  <p id="bDMF">Основная часть коммона — это обычная повседневная комедия (с необычной линзой), которая знакомит нас с персонажами. Причём что удивительно — комедия смешная. Химия между персонажами просто невероятная и это создаёт шутки на грани комедии абсурда, но как же это периодически разрывает... </p>
  <p id="Jvpn">В принципе Грисая во время коммона оборачивает в геги почти любую ситуацию, даже серьёзную, вплоть до того что жажда мужского внимания у героинь высмеивается шутками об их мастурбации на протагониста.</p>
  <p id="MFwy">Но при всём этом как только игрок выбирает конкретный рут — комедия достаточно быстро вымещается серьёзным повествованием, причём это может быть как флешбек на десять часов (да, правда, и он очень даже интересный), так и история о бегах и сражении с врагами в стиле Джона Уика.</p>
  <p id="ICdV">Кстати, о моих словах о &quot;<em>первом&quot; руте...</em></p>
  <h2 id="s4A2">Структура игры</h2>
  <p id="CzR5">Не считая первых двух выборов, которые <strong><em>почти</em></strong> (pro tip: помогайте нуждающимся и не скупитесь на доброе слово) ни на что не влияют, новелла имеет лестничную структуру: это означает, что выход на конкретный рут определяется одним выбором, и в случае &quot;неправильного&quot; выбора, вам дадут следующий выбор уже на следующий же рут и так далее. Графически:</p>
  <pre id="wNek">начало
   |-&gt; первый рут
   |-&gt; второй рут
   |-&gt; третий рут
   |-&gt; четвертый рут
   v
пятый рут</pre>
  <section style="background-color:hsl(hsl(170, 33%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="qFBN">Я настоятельно рекомендую проходить руты в том порядке, в котором игра их вам предоставляет.</p>
  </section>
  <p id="B0cm">Несмотря на свою монструозную длительность, я не могу сказать, что читать новеллу тяжело или что она затянута, за исключением нескольких сцен. Она написана на удивление целостно и внятно, повествование не скачет и в целом очень неохотно перескакивает с главного героя и только по необходимости. В своей сути она до последней запятой следует обычной структуре драмы из трёх актов... кроме рута Мичиру, но о нём чуть-чуть дальше.</p>
  <p id="yTwo">В истории часто были ссылки и цитирования моментов из коммона или предыдущих рутов, что у внимательного читателя вызывает моменты радости в стиле &quot;вот, я помню эту фразу, это было предвестие (foreshadowing)!&quot;, что приятно и ещё больше доказывает грамотность сценаристов.</p>
  <hr />
  <h3 id="aJBg">Рут Мичиру — единственный неработающий рут в игре</h3>
  <p id="B8wa">Это тот рут, который вызовет недоумевание примерно у всех. Не поймите превратно, сам персонаж многим очень нравится, но вот конкретно её рут — простите, ###нутый. Дело не в том, что рут плохой — он просто очень тупой по завязке и реализации и очень сильно структурно выделяется на фоне остальных. </p>
  <p id="jkYB">Все остальные руты следуют структуре из трёх актов драматургии до последней запятой, в них можно примерно понять где ты находишься, сколько ещё осталось и что тебя ждёт; здесь - забудьте, он абсолютно хаотичный, сбивчивый сами происходящие в руте события его структурированию вообще не помогают.</p>
  <p id="udpf">Опять же, рут не плохой — в нём есть пусть и немного дешёвые, но всё же очень сильные сцены и мысли, а развязка, если закрыть глаза на её общий тупизм, — одна из самых сильных сцен в игре. Но основная проблема рута — это противоречия самому себе и почти что паранормальная общая завязка, при том что вся остальная игра придерживается реализма, пусть и иногда такого, в который сложно поверить.</p>
  <p id="01Bo">На все вопросы &quot;почему так&quot; есть достаточно простой ответ — рут доверили непрофессиональному сценаристу, который до этого в основном писал тексты песен и помогал со сценариями по мелочи — иными словами, банальная неопытность. Почему ему рут доверили — вопрос хороший, однако остающийся загадкой по сей день.</p>
  <h2 id="4Bxv">Технические моменты</h2>
  <p id="9hqo">Если вы зайдёте на VNDB и проспойлерите себе список персонажей, то спешу огорчить — в первой игре главными героинями будет только основная пятёрка и никого больше, да и в сиквелах ни у кого больше не будет &quot;полноценного&quot; рута, однако это разговор для следующего раза.</p>
  <hr />
  <p id="KS3p">На Linux / Steam Deck игра работает со скрипом — для запуска (через стим) необходимо выключить DRI3 и включить WINED3D9:</p>
  <pre id="82mr">LIBGL_DRI3_DISABLE=1 PROTON_USE_WINED3D9=1 %command%</pre>
  <p id="67JE">Для запуска не через стим (пиратки бишь) через gamescope (для повышения разрешения через FSR) нужно сделать примерно так (скриптом или каждый раз ручками - ваш выбор):</p>
  <pre id="iCzO">LIBGL_DRI3_DISABLE=1 PROTON_USE_WINED3D9=1 WINE_FULLSCREEN_FSR_STRENGTH=0 WINE_FULLSCREEN_FSR_MODE=ultra INTEL_DEBUG=norbc gamescope -h 576 -w 1024 -H 1080 -f -U -- wine BootMenu.exe</pre>
  <p id="zlwQ">В обоих случаях сразу советую залезть в настройки игры и выключить воспроизведение видео (всё равно не заработает) и включить фикс для отсутствующих превью файлов сохранений.</p>
  <p id="DkIX">И ещё — игра через протон может изредка просто повиснуть при переходе сцен и просто не загрузить новый, так что сохраняйтесь почаще. И даже будучи визуальной новеллой, через протон она способно неплохо так нагрузить не самый мощный ноутбук, так что играть без зарядки рядом крайне не советую, если только не ограничивать ей фпс и потребление процессора вручную.</p>
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="kJxh">Также для игры есть патч, возвращающий вырезанный контент из других версией: <a href="https://vndb.org/r84358" target="_blank">[для Steam версии]</a>, <a href="https://vndb.org/r83746" target="_blank">[для Unrated Edition]</a></p>
  </section>
  <h2 id="scv6">Общие мысли и заключение</h2>
  <p id="2lWK">Грисайя — это немного неоднозначная игра. Она вам либо очень понравится, либо вы её не поймёте и невзлюбите, почему-то чего-то посередине практически не случается. Тем не менее, цифры не врут: это четвёртая по популярности визуальная новелла на VNDB со средним рейтингом 8.4 и я настоятельно советую в неё поиграть.</p>
  <p id="MYyK">Она неидеальна, в ней таки есть некоторые сюжетные дыры, химия местами страдает, но о её недостатках не очень хочется говорить (поэтому они тут не сильно и затронуты), она либо нравится, либо нет.</p>
  <p id="bhaD">Но советую опираясь не на рейтинг, на простой факт — это единичный проект. Игры такого масштаба и качества — огромная редкость, и только лишь за это, как мне кажется, стоит хотя бы попробовать пройти её.</p>
  <section style="background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="f5X7">Крайне советую — единичная новелла, которую стоит попробовать всем.</p>
  </section>
  <hr />
  <p id="MbpH">Спасибо, что прочитали этот малосвязный поток сознания до конца :)</p>

]]></content:encoded></item></channel></rss>