<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Andrey_Dvur</title><link>https://andrey.dvur.me/posts/</link><description>Длинные разборы: инфраструктура, selfhosted, автоматизация</description><language>ru</language><lastBuildDate>Wed, 11 Nov 2026 19:54:22 +0300</lastBuildDate><atom:link href="https://andrey.dvur.me/posts/dzen.xml" rel="self" type="application/rss+xml"/><item><title>ИИ Антиспам на Proxmox Mail Gateway</title><link>https://andrey.dvur.me/posts/2025-11-11-pmg-custom-check/</link><guid isPermaLink="true">https://andrey.dvur.me/posts/2025-11-11-pmg-custom-check/</guid><pubDate>Tue, 11 Nov 2025 00:00:00 +0000</pubDate><category>format-article</category><category>index</category><category>comment-all</category><enclosure url="https://andrey.dvur.me/posts/2025-11-11-pmg-custom-check/featured.png" type="image/jpeg"/><content:encoded><![CDATA[

<h2>Введение 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B2%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p><a href="https://proxmox.com/en/products/proxmox-mail-gateway/overview" target="_blank">Proxmox Mail Gateway</a> - шлюз для электронной почты, который стоит до почтового сервера, фильтрует спам, вирусы и пр.</p>
<p>Про настройку самого Proxmox Mail Gateway документации и видео в интернете достаточно (например <a href="https://www.youtube.com/watch?v=hNZaqlQsT8g" target="_blank">чать 1</a>, <a href="https://www.youtube.com/watch?v=v61TedpzbS4" target="_blank">чать 2</a>).</p>


<h2>Custom check 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#custom-check" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Почему бы дополнительно к спам-фильтрам не добавить AI, подумал я и решил этим заняться.
Напрямую интеграции PMG с какими-либо нейросетями нет, но в документации есть упоминание функции <a href="https://pmg.proxmox.com/pmg-docs/pmg-admin-guide.html#pmgconfig_custom_check" target="_blank">custom check</a> - это скрипт, который выполняется еще до фильтров самого PMG.</p>
<p>Вайб-кодим небольшой скрипт, который будет наши письма анализировать с помощью AI. В качестве провайдера - я использую OpenRouter и небольшую модель mistralai/mistral-small-3.1-24b-instruct.</p>
<p>Используем white-лист чтобы не анализировать доверенные домены и black-лист чтобы повторно не проверять письма с домена, который уже попался на рассылке спама.</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span><span>#!/bin/bash
</span></span></span><span><span><span></span>
</span></span><span><span><span># Custom email checker script for Proxmox Mail Gateway</span>
</span></span><span><span><span># Uses OpenRouter API to analyze emails for spam content</span>
</span></span><span><span>
</span></span><span><span><span>set</span> -euo pipefail  <span># Exit on error, undefined variables, and pipe failures</span>
</span></span><span><span>
</span></span><span><span><span># Log file configuration</span>
</span></span><span><span><span>LOG_FILE</span><span>=</span><span>&#34;/var/log/pmg-custom-check.log&#34;</span>
</span></span><span><span><span>LOG_MAX_SIZE</span><span>=</span><span>10485760</span>  <span># 10MB</span>
</span></span><span><span>
</span></span><span><span><span># Rotate log file if it exceeds max size</span>
</span></span><span><span>rotate_log<span>()</span> <span>{</span>
</span></span><span><span>    <span>if</span> <span>[</span> -f <span>&#34;</span><span>$LOG_FILE</span><span>&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>        <span>local</span> <span>size</span><span>=</span><span>$(</span>stat -c%s <span>&#34;</span><span>$LOG_FILE</span><span>&#34;</span> 2&gt;/dev/null <span>||</span> <span>echo</span> 0<span>)</span>
</span></span><span><span>        <span>if</span> <span>[</span> <span>&#34;</span><span>$size</span><span>&#34;</span> -gt <span>&#34;</span><span>$LOG_MAX_SIZE</span><span>&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>            mv <span>&#34;</span><span>$LOG_FILE</span><span>&#34;</span> <span>&#34;</span><span>${</span><span>LOG_FILE</span><span>}</span><span>.old&#34;</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>        <span>fi</span>
</span></span><span><span>    <span>fi</span>
</span></span><span><span><span>}</span>
</span></span><span><span>
</span></span><span><span><span># Log function to write to both STDERR (mail.log) and log file</span>
</span></span><span><span>log<span>()</span> <span>{</span>
</span></span><span><span>    <span>local</span> message
</span></span><span><span>    <span>message</span><span>=</span><span>&#34;</span><span>$(</span>date <span>&#39;+%Y-%m-%d %H:%M:%S&#39;</span><span>)</span><span> - </span><span>$1</span><span>&#34;</span>
</span></span><span><span>    <span># Explicitly write to file descriptor 2 (stderr) and handle any errors</span>
</span></span><span><span>    <span>{</span>
</span></span><span><span>        <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;</span><span>$message</span><span>&#34;</span> &gt;<span>&amp;</span><span>2</span>
</span></span><span><span>    <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>    rotate_log
</span></span><span><span>    <span>{</span>
</span></span><span><span>        <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;</span><span>$message</span><span>&#34;</span> &gt;&gt; <span>&#34;</span><span>$LOG_FILE</span><span>&#34;</span>
</span></span><span><span>    <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span><span>}</span>
</span></span><span><span>
</span></span><span><span><span># Function to extract email headers</span>
</span></span><span><span>extract_email_headers<span>()</span> <span>{</span>
</span></span><span><span>    <span>local</span> <span>email_file</span><span>=</span><span>&#34;</span><span>$1</span><span>&#34;</span>
</span></span><span><span>    
</span></span><span><span>    <span># Extract sender (From header) and decode base64 encoding if present</span>
</span></span><span><span>    <span>local</span> <span>raw_sender</span><span>=</span><span>$(</span>grep -m <span>1</span> <span>&#39;^From:&#39;</span> <span>&#34;</span><span>$email_file</span><span>&#34;</span> 2&gt;/dev/null <span>|</span> sed <span>&#39;s/^From: *//&#39;</span> <span>||</span> <span>echo</span> <span>&#34;unknown&#34;</span><span>)</span>
</span></span><span><span>    
</span></span><span><span>    <span># Decode MIME encoded words (e.g., =?UTF-8?B?...?= or =?UTF-8?Q?...?=)</span>
</span></span><span><span>    <span>local</span> <span>sender</span><span>=</span><span>&#34;</span><span>$raw_sender</span><span>&#34;</span>
</span></span><span><span>    <span>if</span> <span>[[</span> <span>&#34;</span><span>$raw_sender</span><span>&#34;</span> <span>==</span> *<span>=</span>?* <span>]]</span><span>;</span> <span>then</span>
</span></span><span><span>        <span># Use Python for more reliable MIME decoding</span>
</span></span><span><span>        <span>if</span> <span>command</span> -v python3 &gt;/dev/null 2&gt;<span>&amp;</span>1<span>;</span> <span>then</span>
</span></span><span><span>            <span># Create temporary file approach to avoid complex quoting</span>
</span></span><span><span>            <span>local</span> <span>temp_py_script</span><span>=</span><span>&#34;/tmp/pmg_decode_</span><span>$$</span><span>.py&#34;</span>
</span></span><span><span>            cat &gt; <span>&#34;</span><span>$temp_py_script</span><span>&#34;</span> <span>&lt;&lt; &#39;EOF_PY&#39;
</span></span></span><span><span><span>import sys
</span></span></span><span><span><span>from email.header import decode_header
</span></span></span><span><span><span>raw_sender = sys.argv[1] if len(sys.argv) &gt; 1 else &#34;&#34;
</span></span></span><span><span><span>decoded = decode_header(raw_sender)
</span></span></span><span><span><span>result = []
</span></span></span><span><span><span>for part, encoding in decoded:
</span></span></span><span><span><span>    if isinstance(part, bytes):
</span></span></span><span><span><span>        if encoding:
</span></span></span><span><span><span>            result.append(part.decode(encoding))
</span></span></span><span><span><span>        else:
</span></span></span><span><span><span>            result.append(part.decode(&#39;utf-8&#39;))
</span></span></span><span><span><span>    else:
</span></span></span><span><span><span>        result.append(part)
</span></span></span><span><span><span>print(&#39;&#39;.join(result))
</span></span></span><span><span><span>EOF_PY</span>
</span></span><span><span>            <span># Ensure the Python script executes safely</span>
</span></span><span><span>            <span>if</span> <span>[</span> -f <span>&#34;</span><span>$temp_py_script</span><span>&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>                <span>sender</span><span>=</span><span>$(</span>python3 <span>&#34;</span><span>$temp_py_script</span><span>&#34;</span> <span>&#34;</span><span>$raw_sender</span><span>&#34;</span> 2&gt;/dev/null<span>)</span>
</span></span><span><span>                <span># If Python decoding failed, use original value</span>
</span></span><span><span>                <span>if</span> <span>[</span> <span>$?</span> -ne <span>0</span> <span>]</span> <span>||</span> <span>[</span> -z <span>&#34;</span><span>$sender</span><span>&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>                    <span>sender</span><span>=</span><span>&#34;</span><span>$raw_sender</span><span>&#34;</span>
</span></span><span><span>                <span>fi</span>
</span></span><span><span>                rm -f <span>&#34;</span><span>$temp_py_script</span><span>&#34;</span>
</span></span><span><span>            <span>else</span>
</span></span><span><span>                <span># Fallback if temp script creation failed</span>
</span></span><span><span>                <span>sender</span><span>=</span><span>&#34;</span><span>$raw_sender</span><span>&#34;</span>
</span></span><span><span>            <span>fi</span>
</span></span><span><span>        <span>fi</span>
</span></span><span><span>    <span>fi</span>
</span></span><span><span>    
</span></span><span><span>    <span># Extract receiver (To header)</span>
</span></span><span><span>    <span>local</span> <span>receiver</span><span>=</span><span>$(</span>grep -m <span>1</span> <span>&#39;^To:&#39;</span> <span>&#34;</span><span>$email_file</span><span>&#34;</span> 2&gt;/dev/null <span>|</span> sed <span>&#39;s/^To: *//&#39;</span> <span>||</span> <span>echo</span> <span>&#34;unknown&#34;</span><span>)</span>
</span></span><span><span>    
</span></span><span><span>    <span># Extract subject (Subject header)</span>
</span></span><span><span>    <span>local</span> <span>subject</span><span>=</span><span>$(</span>grep -m <span>1</span> <span>&#39;^Subject:&#39;</span> <span>&#34;</span><span>$email_file</span><span>&#34;</span> 2&gt;/dev/null <span>|</span> sed <span>&#39;s/^Subject: *//&#39;</span> <span>||</span> <span>echo</span> <span>&#34;no subject&#34;</span><span>)</span>
</span></span><span><span>    
</span></span><span><span>    <span># Extract message ID (Message-ID header) - keeping for internal use only</span>
</span></span><span><span>    <span>local</span> <span>message_id</span><span>=</span><span>$(</span>grep -m <span>1</span> <span>&#39;^Message-ID:&#39;</span> <span>&#34;</span><span>$email_file</span><span>&#34;</span> 2&gt;/dev/null <span>|</span> sed <span>&#39;s/^Message-ID: *//&#39;</span> <span>||</span> <span>echo</span> <span>&#34;no message-id&#34;</span><span>)</span>
</span></span><span><span>    
</span></span><span><span>    <span># Print the extracted headers for return</span>
</span></span><span><span>    <span>echo</span> <span>&#34;</span><span>$sender</span><span>|</span><span>$receiver</span><span>|</span><span>$subject</span><span>|</span><span>$message_id</span><span>&#34;</span>
</span></span><span><span><span>}</span>
</span></span><span><span>
</span></span><span><span><span># Print usage and exit if arguments are incorrect</span>
</span></span><span><span><span>if</span> <span>[</span> <span>&#34;</span><span>$#</span><span>&#34;</span> -ne <span>2</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>    log <span>&#34;usage: </span><span>$0</span><span> APIVERSION QUEUEFILENAME&#34;</span>
</span></span><span><span>    <span>exit</span> <span>1</span>
</span></span><span><span><span>fi</span>
</span></span><span><span>
</span></span><span><span><span>apiver</span><span>=</span><span>&#34;</span><span>$1</span><span>&#34;</span>
</span></span><span><span><span>queue_file</span><span>=</span><span>&#34;</span><span>$2</span><span>&#34;</span>
</span></span><span><span>
</span></span><span><span><span># Check API version</span>
</span></span><span><span><span>if</span> <span>[</span> <span>&#34;</span><span>$apiver</span><span>&#34;</span> !<span>=</span> <span>&#34;v1&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>    log <span>&#34;wrong APIVERSION: </span><span>$apiver</span><span>&#34;</span>
</span></span><span><span>    <span>exit</span> <span>2</span>
</span></span><span><span><span>fi</span>
</span></span><span><span>
</span></span><span><span><span># Extract email headers before processing (needed for logging in error cases)</span>
</span></span><span><span><span>headers_info</span><span>=</span><span>$(</span>extract_email_headers <span>&#34;</span><span>$queue_file</span><span>&#34;</span><span>)</span>
</span></span><span><span><span>sender</span><span>=</span><span>$(</span><span>echo</span> <span>&#34;</span><span>$headers_info</span><span>&#34;</span> <span>|</span> cut -d<span>&#39;|&#39;</span> -f1<span>)</span>
</span></span><span><span><span>receiver</span><span>=</span><span>$(</span><span>echo</span> <span>&#34;</span><span>$headers_info</span><span>&#34;</span> <span>|</span> cut -d<span>&#39;|&#39;</span> -f2<span>)</span>
</span></span><span><span><span>subject</span><span>=</span><span>$(</span><span>echo</span> <span>&#34;</span><span>$headers_info</span><span>&#34;</span> <span>|</span> cut -d<span>&#39;|&#39;</span> -f3<span>)</span>
</span></span><span><span><span>message_id</span><span>=</span><span>$(</span><span>echo</span> <span>&#34;</span><span>$headers_info</span><span>&#34;</span> <span>|</span> cut -d<span>&#39;|&#39;</span> -f4<span>)</span>
</span></span><span><span>
</span></span><span><span><span># Whitelist/Blacklist functionality</span>
</span></span><span><span>check_whitelist_blacklist<span>()</span> <span>{</span>
</span></span><span><span>    <span>local</span> <span>sender</span><span>=</span><span>&#34;</span><span>$1</span><span>&#34;</span>
</span></span><span><span>    <span>local</span> <span>whitelist_file</span><span>=</span><span>&#34;</span><span>$(</span>dirname <span>&#34;</span><span>$0</span><span>&#34;</span><span>)</span><span>/whitelist.txt&#34;</span>
</span></span><span><span>    <span>local</span> <span>blacklist_file</span><span>=</span><span>&#34;</span><span>$(</span>dirname <span>&#34;</span><span>$0</span><span>&#34;</span><span>)</span><span>/blacklist.txt&#34;</span>
</span></span><span><span>    
</span></span><span><span>    <span># Check if sender matches whitelist</span>
</span></span><span><span>    <span>if</span> <span>[</span> -f <span>&#34;</span><span>$whitelist_file</span><span>&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>        <span>while</span> <span>IFS</span><span>=</span> <span>read</span> -r pattern<span>;</span> <span>do</span>
</span></span><span><span>            <span># Skip empty lines</span>
</span></span><span><span>            <span>[</span> -z <span>&#34;</span><span>$pattern</span><span>&#34;</span> <span>]</span> <span>&amp;&amp;</span> <span>continue</span>
</span></span><span><span>            <span>if</span> <span>echo</span> <span>&#34;</span><span>$sender</span><span>&#34;</span> <span>|</span> grep -qi <span>&#34;</span><span>$pattern</span><span>&#34;</span><span>;</span> <span>then</span>
</span></span><span><span>                log <span>&#34;Sender in whitelist: </span><span>$sender</span><span> - returning SCORE: 0&#34;</span>
</span></span><span><span>                <span>{</span>
</span></span><span><span>                    <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;SCORE: 0&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>                <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>                <span>exit</span> <span>0</span>
</span></span><span><span>            <span>fi</span>
</span></span><span><span>        <span>done</span> &lt; <span>&#34;</span><span>$whitelist_file</span><span>&#34;</span>
</span></span><span><span>    <span>fi</span>
</span></span><span><span>    
</span></span><span><span>    <span># Check if sender matches blacklist</span>
</span></span><span><span>    <span>if</span> <span>[</span> -f <span>&#34;</span><span>$blacklist_file</span><span>&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>        <span>while</span> <span>IFS</span><span>=</span> <span>read</span> -r pattern<span>;</span> <span>do</span>
</span></span><span><span>            <span># Skip empty lines</span>
</span></span><span><span>            <span>[</span> -z <span>&#34;</span><span>$pattern</span><span>&#34;</span> <span>]</span> <span>&amp;&amp;</span> <span>continue</span>
</span></span><span><span>            <span>if</span> <span>echo</span> <span>&#34;</span><span>$sender</span><span>&#34;</span> <span>|</span> grep -qi <span>&#34;</span><span>$pattern</span><span>&#34;</span><span>;</span> <span>then</span>
</span></span><span><span>                log <span>&#34;Sender in blacklist: </span><span>$sender</span><span> - returning SCORE: 10&#34;</span>
</span></span><span><span>                <span>{</span>
</span></span><span><span>                    <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;SCORE: 10&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>                <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>                <span>exit</span> <span>0</span>
</span></span><span><span>            <span>fi</span>
</span></span><span><span>        <span>done</span> &lt; <span>&#34;</span><span>$blacklist_file</span><span>&#34;</span>
</span></span><span><span>    <span>fi</span>
</span></span><span><span><span>}</span>
</span></span><span><span>
</span></span><span><span><span># Check whitelist/blacklist before proceeding with API check</span>
</span></span><span><span>check_whitelist_blacklist <span>&#34;</span><span>$sender</span><span>&#34;</span>
</span></span><span><span>
</span></span><span><span><span># Check if input file exists</span>
</span></span><span><span><span>if</span> <span>[</span> ! -f <span>&#34;</span><span>$queue_file</span><span>&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>    log <span>&#34;queue file does not exist: </span><span>$queue_file</span><span>&#34;</span>
</span></span><span><span>    <span># Output API version and result for missing file to STDOUT explicitly</span>
</span></span><span><span>    <span>{</span>
</span></span><span><span>        <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;v1&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>        <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;OK&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>    <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>    <span>exit</span> <span>0</span>
</span></span><span><span><span>fi</span>
</span></span><span><span>
</span></span><span><span><span># Configuration for OpenRouter API</span>
</span></span><span><span><span>OPENROUTER_API_KEY</span><span>=</span><span>&#34;sk-or-v1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&#34;</span>
</span></span><span><span><span>OPENROUTER_MODEL</span><span>=</span><span>&#34;mistralai/mistral-small-3.1-24b-instruct&#34;</span>
</span></span><span><span>
</span></span><span><span><span>if</span> <span>[</span> -z <span>&#34;</span><span>$OPENROUTER_API_KEY</span><span>&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>    log <span>&#34;ERROR: OPENROUTER_API_KEY environment variable not set, cannot proceed with API check - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>&#34;</span>
</span></span><span><span>    <span># Output API version and result if no API key to STDOUT explicitly</span>
</span></span><span><span>    <span>{</span>
</span></span><span><span>        <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;v1&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>        <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;OK&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>    <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>    <span>exit</span> <span>0</span>
</span></span><span><span><span>fi</span>
</span></span><span><span>
</span></span><span><span><span># Output API version once all validation is done to STDOUT explicitly</span>
</span></span><span><span><span>{</span>
</span></span><span><span>    <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;v1&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span><span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>
</span></span><span><span><span># Read the email content</span>
</span></span><span><span><span>email_content</span><span>=</span><span>&#34;&#34;</span>
</span></span><span><span><span>if</span> <span>[</span> -r <span>&#34;</span><span>$queue_file</span><span>&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>    <span>email_content</span><span>=</span><span>$(</span>cat <span>&#34;</span><span>$queue_file</span><span>&#34;</span><span>)</span>
</span></span><span><span>    log <span>&#34;Successfully read email content - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>, Size: </span><span>$(</span><span>echo</span> -n <span>&#34;</span><span>$email_content</span><span>&#34;</span> <span>|</span> wc -c<span>)</span><span> bytes&#34;</span>
</span></span><span><span><span>else</span>
</span></span><span><span>    log <span>&#34;ERROR: cannot read queue file: </span><span>$queue_file</span><span> - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>&#34;</span>
</span></span><span><span>    <span>{</span>
</span></span><span><span>        <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;OK&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>    <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>    <span>exit</span> <span>0</span>
</span></span><span><span><span>fi</span>
</span></span><span><span>
</span></span><span><span><span># Function to decode MIME encoded subject (similar to the one used in extract_email_headers)</span>
</span></span><span><span>decode_mime_subject<span>()</span> <span>{</span>
</span></span><span><span>    <span>local</span> <span>raw_subject</span><span>=</span><span>&#34;</span><span>$1</span><span>&#34;</span>
</span></span><span><span>    <span>local</span> <span>decoded_subject</span><span>=</span><span>&#34;</span><span>$raw_subject</span><span>&#34;</span>
</span></span><span><span>    
</span></span><span><span>    <span># Check if subject is MIME encoded (contains =?...?= pattern)</span>
</span></span><span><span>    <span>if</span> <span>[[</span> <span>&#34;</span><span>$raw_subject</span><span>&#34;</span> <span>==</span> *<span>=</span>?* <span>]]</span><span>;</span> <span>then</span>
</span></span><span><span>        <span># Use Python for reliable MIME decoding</span>
</span></span><span><span>        <span>if</span> <span>command</span> -v python3 &gt;/dev/null 2&gt;<span>&amp;</span>1<span>;</span> <span>then</span>
</span></span><span><span>            <span>local</span> <span>temp_py_script</span><span>=</span><span>&#34;/tmp/pmg_decode_subject_</span><span>$$</span><span>.py&#34;</span>
</span></span><span><span>            cat &gt; <span>&#34;</span><span>$temp_py_script</span><span>&#34;</span> <span>&lt;&lt; &#39;EOF_PY&#39;
</span></span></span><span><span><span>import sys
</span></span></span><span><span><span>from email.header import decode_header
</span></span></span><span><span><span>raw_subject = sys.argv[1] if len(sys.argv) &gt; 1 else &#34;&#34;
</span></span></span><span><span><span>decoded = decode_header(raw_subject)
</span></span></span><span><span><span>result = []
</span></span></span><span><span><span>for part, encoding in decoded:
</span></span></span><span><span><span>    if isinstance(part, bytes):
</span></span></span><span><span><span>        if encoding:
</span></span></span><span><span><span>            result.append(part.decode(encoding))
</span></span></span><span><span><span>        else:
</span></span></span><span><span><span>            result.append(part.decode(&#39;utf-8&#39;))
</span></span></span><span><span><span>    else:
</span></span></span><span><span><span>        result.append(part)
</span></span></span><span><span><span>print(&#39;&#39;.join(result))
</span></span></span><span><span><span>EOF_PY</span>
</span></span><span><span>            <span>if</span> <span>[</span> -f <span>&#34;</span><span>$temp_py_script</span><span>&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>                <span>decoded_subject</span><span>=</span><span>$(</span>python3 <span>&#34;</span><span>$temp_py_script</span><span>&#34;</span> <span>&#34;</span><span>$raw_subject</span><span>&#34;</span> 2&gt;/dev/null<span>)</span>
</span></span><span><span>                <span>if</span> <span>[</span> <span>$?</span> -ne <span>0</span> <span>]</span> <span>||</span> <span>[</span> -z <span>&#34;</span><span>$decoded_subject</span><span>&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>                    <span>decoded_subject</span><span>=</span><span>&#34;</span><span>$raw_subject</span><span>&#34;</span>
</span></span><span><span>                <span>fi</span>
</span></span><span><span>                rm -f <span>&#34;</span><span>$temp_py_script</span><span>&#34;</span>
</span></span><span><span>            <span>fi</span>
</span></span><span><span>        <span>fi</span>
</span></span><span><span>    <span>fi</span>
</span></span><span><span>    
</span></span><span><span>    <span>echo</span> <span>&#34;</span><span>$decoded_subject</span><span>&#34;</span>
</span></span><span><span><span>}</span>
</span></span><span><span>
</span></span><span><span><span># Decode the subject for logging</span>
</span></span><span><span><span>decoded_subject</span><span>=</span><span>$(</span>decode_mime_subject <span>&#34;</span><span>$subject</span><span>&#34;</span><span>)</span>
</span></span><span><span>
</span></span><span><span><span># Log email details before processing with decoded subject</span>
</span></span><span><span>log <span>&#34;Processing email - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>, Subject: </span><span>$decoded_subject</span><span>&#34;</span>
</span></span><span><span>
</span></span><span><span><span># Limit email size to prevent API errors (e.g., 100KB limit)</span>
</span></span><span><span><span>EMAIL_SIZE_LIMIT</span><span>=</span><span>52400</span>  <span># 100KB in bytes (100 * 1024)</span>
</span></span><span><span><span>email_size</span><span>=</span><span>$(</span><span>echo</span> -n <span>&#34;</span><span>$email_content</span><span>&#34;</span> <span>|</span> wc -c<span>)</span>
</span></span><span><span>
</span></span><span><span><span>if</span> <span>[</span> <span>&#34;</span><span>$email_size</span><span>&#34;</span> -gt <span>&#34;</span><span>$EMAIL_SIZE_LIMIT</span><span>&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>    log <span>&#34;Email too large (</span><span>$email_size</span><span> bytes), truncating to </span><span>$EMAIL_SIZE_LIMIT</span><span> bytes - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>&#34;</span>
</span></span><span><span>    <span># Truncate email content, handling potential pipe issues</span>
</span></span><span><span>    <span>email_content</span><span>=</span><span>$(</span><span>echo</span> <span>&#34;</span><span>$email_content</span><span>&#34;</span> <span>|</span> head -c <span>&#34;</span><span>$EMAIL_SIZE_LIMIT</span><span>&#34;</span> 2&gt;/dev/null <span>||</span> <span>echo</span> <span>&#34;</span><span>${</span><span>email_content</span><span>:</span><span>0</span><span>:</span><span>$EMAIL_SIZE_LIMIT</span><span>}</span><span>&#34;</span><span>)</span>
</span></span><span><span><span>fi</span>
</span></span><span><span>
</span></span><span><span><span># Prepare the request to OpenRouter API</span>
</span></span><span><span><span>request_body</span><span>=</span><span>$(</span>cat <span>&lt;&lt;EOF
</span></span></span><span><span><span>{
</span></span></span><span><span><span>  &#34;model&#34;: &#34;$OPENROUTER_MODEL&#34;,
</span></span></span><span><span><span>  &#34;messages&#34;: [
</span></span></span><span><span><span>    {
</span></span></span><span><span><span>      &#34;role&#34;: &#34;system&#34;,
</span></span></span><span><span><span>      &#34;content&#34;: &#34;You are an email spam classifier. Analyze the following email and determine if it is spam. Respond with a JSON object containing &#39;is_spam&#39; (boolean) and &#39;confidence&#39; (number between 0 and 1) and &#39;reason&#39; (string explaining why). Be strict in your assessment - only mark as spam if there are clear signs of unwanted or malicious content.&#34;
</span></span></span><span><span><span>    },
</span></span></span><span><span><span>    {
</span></span></span><span><span><span>      &#34;role&#34;: &#34;user&#34;,
</span></span></span><span><span><span>      &#34;content&#34;: &#34;Please analyze this email for spam content:\n\n$email_content&#34;
</span></span></span><span><span><span>    }
</span></span></span><span><span><span>  ],
</span></span></span><span><span><span>  &#34;provider&#34;: {
</span></span></span><span><span><span>    &#34;sort&#34;: &#34;price&#34;
</span></span></span><span><span><span>  }
</span></span></span><span><span><span>}
</span></span></span><span><span><span>EOF</span>
</span></span><span><span><span>)</span>
</span></span><span><span>
</span></span><span><span><span># Log that we&#39;re about to make the API call</span>
</span></span><span><span>log <span>&#34;About to call OpenRouter API - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>&#34;</span>
</span></span><span><span>
</span></span><span><span><span># Use cURL to call the OpenRouter API</span>
</span></span><span><span><span>response</span><span>=</span><span>$(</span>curl -s -X POST <span>\
</span></span></span><span><span><span></span>  -H <span>&#34;Authorization: Bearer </span><span>$OPENROUTER_API_KEY</span><span>&#34;</span> <span>\
</span></span></span><span><span><span></span>  -H <span>&#34;Content-Type: application/json&#34;</span> <span>\
</span></span></span><span><span><span></span>  -d <span>&#34;</span><span>$request_body</span><span>&#34;</span> <span>\
</span></span></span><span><span><span></span>  <span>&#34;https://openrouter.ai/api/v1/chat/completions&#34;</span> <span>\
</span></span></span><span><span><span></span>  --max-time 240<span>)</span>  <span># 4 minute timeout to stay under the 5-minute limit</span>
</span></span><span><span>
</span></span><span><span><span>if</span> <span>[</span> <span>$?</span> -ne <span>0</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>    log <span>&#34;Failed to get response from OpenRouter API - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>&#34;</span>
</span></span><span><span>    <span>{</span>
</span></span><span><span>        <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;OK&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>    <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>    <span>exit</span> <span>0</span>
</span></span><span><span><span>fi</span>
</span></span><span><span>
</span></span><span><span><span>if</span> <span>[</span> -z <span>&#34;</span><span>$response</span><span>&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>    log <span>&#34;Empty response from OpenRouter API - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>&#34;</span>
</span></span><span><span>    <span>{</span>
</span></span><span><span>        <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;OK&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>    <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>    <span>exit</span> <span>0</span>
</span></span><span><span><span>fi</span>
</span></span><span><span>
</span></span><span><span><span># Check if jq is available before using it</span>
</span></span><span><span><span>if</span> ! <span>command</span> -v jq &gt;/dev/null 2&gt;<span>&amp;</span>1<span>;</span> <span>then</span>
</span></span><span><span>    log <span>&#34;ERROR: jq command not found - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>&#34;</span>
</span></span><span><span>    <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;OK&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>    <span>exit</span> <span>0</span>
</span></span><span><span><span>fi</span>
</span></span><span><span>
</span></span><span><span><span>response_size</span><span>=</span><span>$(</span><span>echo</span> -n <span>&#34;</span><span>$response</span><span>&#34;</span> <span>|</span> wc -c<span>)</span>
</span></span><span><span>log <span>&#34;Successfully received response from OpenRouter API - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>, Response size: </span><span>$response_size</span><span> bytes&#34;</span>
</span></span><span><span>
</span></span><span><span><span># Check if the response contains an error, but first check if jq is available</span>
</span></span><span><span><span>if</span> <span>command</span> -v jq &gt;/dev/null 2&gt;<span>&amp;</span>1<span>;</span> <span>then</span>
</span></span><span><span>    <span>if</span> <span>echo</span> <span>&#34;</span><span>$response</span><span>&#34;</span> <span>|</span> jq -e <span>&#39;.error&#39;</span> &gt;/dev/null 2&gt;<span>&amp;</span>1<span>;</span> <span>then</span>
</span></span><span><span>        <span>error_message</span><span>=</span><span>$(</span><span>echo</span> <span>&#34;</span><span>$response</span><span>&#34;</span> <span>|</span> jq -r <span>&#39;.error.message // &#34;Unknown API error&#34;&#39;</span><span>)</span>
</span></span><span><span>        <span>error_code</span><span>=</span><span>$(</span><span>echo</span> <span>&#34;</span><span>$response</span><span>&#34;</span> <span>|</span> jq -r <span>&#39;.error.code // &#34;Unknown code&#34;&#39;</span><span>)</span>
</span></span><span><span>        log <span>&#34;OpenRouter API error - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>, Error: </span><span>$error_message</span><span>, Code: </span><span>$error_code</span><span>&#34;</span>
</span></span><span><span>        <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;OK&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>        <span>exit</span> <span>0</span>
</span></span><span><span>    <span>fi</span>
</span></span><span><span><span>else</span>
</span></span><span><span>    log <span>&#34;ERROR: jq command not found during error checking - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>&#34;</span>
</span></span><span><span>    <span>{</span>
</span></span><span><span>        <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;OK&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>    <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>    <span>exit</span> <span>0</span>
</span></span><span><span><span>fi</span>
</span></span><span><span>
</span></span><span><span><span># Extract the AI&#39;s response (need to extract the text content from the completion)</span>
</span></span><span><span><span>ai_response</span><span>=</span><span>$(</span><span>echo</span> <span>&#34;</span><span>$response</span><span>&#34;</span> <span>|</span> jq -r <span>&#39;.choices[0].message.content&#39;</span> 2&gt;/dev/null<span>)</span>
</span></span><span><span>
</span></span><span><span><span>if</span> <span>[</span> -z <span>&#34;</span><span>$ai_response</span><span>&#34;</span> <span>]</span> <span>||</span> <span>[</span> <span>&#34;</span><span>$ai_response</span><span>&#34;</span> <span>=</span> <span>&#34;null&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>    log <span>&#34;Could not extract response from OpenRouter API - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>, Response: </span><span>$response</span><span>&#34;</span>
</span></span><span><span>    <span>{</span>
</span></span><span><span>        <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;OK&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>    <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>    <span>exit</span> <span>0</span>
</span></span><span><span><span>fi</span>
</span></span><span><span>
</span></span><span><span><span># Check if sed is available before extracting JSON</span>
</span></span><span><span><span>if</span> ! <span>command</span> -v sed &gt;/dev/null 2&gt;<span>&amp;</span>1<span>;</span> <span>then</span>
</span></span><span><span>    log <span>&#34;ERROR: sed command not found - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>&#34;</span>
</span></span><span><span>    <span>{</span>
</span></span><span><span>        <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;OK&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>    <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>    <span>exit</span> <span>0</span>
</span></span><span><span><span>fi</span>
</span></span><span><span>
</span></span><span><span><span># Check if sed is available before extracting JSON</span>
</span></span><span><span><span>if</span> ! <span>command</span> -v sed &gt;/dev/null 2&gt;<span>&amp;</span>1<span>;</span> <span>then</span>
</span></span><span><span>    log <span>&#34;ERROR: sed command not found - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>&#34;</span>
</span></span><span><span>    <span>{</span>
</span></span><span><span>        <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;OK&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>    <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>    <span>exit</span> <span>0</span>
</span></span><span><span><span>fi</span>
</span></span><span><span>
</span></span><span><span><span># Try to extract JSON from the AI response (sometimes it&#39;s wrapped in markdown or additional text)</span>
</span></span><span><span><span># Handle both markdown-wrapped JSON (```json ... ```) and plain JSON</span>
</span></span><span><span><span>json_response</span><span>=</span><span>&#34;&#34;</span>
</span></span><span><span><span>if</span> <span>echo</span> <span>&#34;</span><span>$ai_response</span><span>&#34;</span> <span>|</span> grep -q <span>&#39;```json&#39;</span><span>;</span> <span>then</span>
</span></span><span><span>    <span># Extract JSON from markdown code block</span>
</span></span><span><span>    <span>json_response</span><span>=</span><span>$(</span><span>echo</span> <span>&#34;</span><span>$ai_response</span><span>&#34;</span> <span>|</span> sed -n <span>&#39;/```json/,/```/p&#39;</span> <span>|</span> head -n -1 <span>|</span> tail -n +2<span>)</span>
</span></span><span><span><span>elif</span> <span>echo</span> <span>&#34;</span><span>$ai_response</span><span>&#34;</span> <span>|</span> grep -E -q <span>&#39;\{.*\}&#39;</span><span>;</span> <span>then</span>
</span></span><span><span>    <span># Extract first JSON object found</span>
</span></span><span><span>    <span>json_response</span><span>=</span><span>$(</span><span>echo</span> <span>&#34;</span><span>$ai_response</span><span>&#34;</span> <span>|</span> sed -n <span>&#39;/{/,/}/p&#39;</span> <span>|</span> sed -n 1p<span>)</span>
</span></span><span><span><span>fi</span>
</span></span><span><span>
</span></span><span><span><span># If extraction failed using above methods, try more flexible approach</span>
</span></span><span><span><span>if</span> <span>[</span> -z <span>&#34;</span><span>$json_response</span><span>&#34;</span> <span>]</span> <span>||</span> <span>[</span> <span>&#34;</span><span>$json_response</span><span>&#34;</span> <span>=</span> <span>&#34;null&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>    <span># Find content between first { and last }</span>
</span></span><span><span>    <span>if</span> <span>command</span> -v grep &gt;/dev/null 2&gt;<span>&amp;</span>1<span>;</span> <span>then</span>
</span></span><span><span>        <span>if</span> <span>echo</span> <span>&#34;</span><span>$ai_response</span><span>&#34;</span> <span>|</span> grep -o <span>&#39;{.*}&#39;</span> &gt;/dev/null 2&gt;<span>&amp;</span>1<span>;</span> <span>then</span>
</span></span><span><span>            <span>json_response</span><span>=</span><span>$(</span><span>echo</span> <span>&#34;</span><span>$ai_response</span><span>&#34;</span> <span>|</span> grep -o <span>&#39;{[^}]*}&#39;</span> <span>|</span> head -n 1<span>)</span>
</span></span><span><span>        <span>fi</span>
</span></span><span><span>    <span>else</span>
</span></span><span><span>        log <span>&#34;ERROR: grep command not found during JSON extraction - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>&#34;</span>
</span></span><span><span>        <span>{</span>
</span></span><span><span>            <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;OK&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>        <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>        <span>exit</span> <span>0</span>
</span></span><span><span>    <span>fi</span>
</span></span><span><span><span>fi</span>
</span></span><span><span>
</span></span><span><span><span># Store the OpenRouter response for later use</span>
</span></span><span><span><span>openrouter_analysis</span><span>=</span><span>&#34;</span><span>$ai_response</span><span>&#34;</span>
</span></span><span><span>
</span></span><span><span><span># Log the complete OpenRouter analysis result</span>
</span></span><span><span>log <span>&#34;OpenRouter Analysis Result - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>, Result: </span><span>$ai_response</span><span>&#34;</span>
</span></span><span><span>
</span></span><span><span><span># Detailed logging of the analysis results</span>
</span></span><span><span><span>if</span> <span>[</span> -n <span>&#34;</span><span>$json_response</span><span>&#34;</span> <span>]</span> <span>&amp;&amp;</span> <span>[</span> <span>&#34;</span><span>$json_response</span><span>&#34;</span> !<span>=</span> <span>&#34;null&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>    <span># Check if jq is still available before parsing JSON response</span>
</span></span><span><span>    <span>if</span> <span>command</span> -v jq &gt;/dev/null 2&gt;<span>&amp;</span>1<span>;</span> <span>then</span>
</span></span><span><span>        <span>is_spam</span><span>=</span><span>$(</span><span>echo</span> <span>&#34;</span><span>$json_response</span><span>&#34;</span> <span>|</span> jq -r <span>&#39;.is_spam&#39;</span> 2&gt;/dev/null<span>)</span>
</span></span><span><span>        <span>confidence</span><span>=</span><span>$(</span><span>echo</span> <span>&#34;</span><span>$json_response</span><span>&#34;</span> <span>|</span> jq -r <span>&#39;.confidence&#39;</span> 2&gt;/dev/null<span>)</span>
</span></span><span><span>        <span>reason</span><span>=</span><span>$(</span><span>echo</span> <span>&#34;</span><span>$json_response</span><span>&#34;</span> <span>|</span> jq -r <span>&#39;.reason&#39;</span> 2&gt;/dev/null<span>)</span>
</span></span><span><span>        
</span></span><span><span>        log <span>&#34;Spam Analysis Result - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>, Is Spam: </span><span>$is_spam</span><span>, Confidence: </span><span>$confidence</span><span>, Reason: </span><span>$reason</span><span>&#34;</span>
</span></span><span><span>    <span>else</span>
</span></span><span><span>        log <span>&#34;ERROR: jq command not found during JSON parsing - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>&#34;</span>
</span></span><span><span>        <span>{</span>
</span></span><span><span>            <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;OK&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>        <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>        <span>exit</span> <span>0</span>
</span></span><span><span>    <span>fi</span>
</span></span><span><span><span>else</span>
</span></span><span><span>    log <span>&#34;Spam Analysis Result - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>, Plain Text Analysis: </span><span>$ai_response</span><span>&#34;</span>
</span></span><span><span><span>fi</span>
</span></span><span><span>
</span></span><span><span><span>if</span> <span>[</span> -n <span>&#34;</span><span>$json_response</span><span>&#34;</span> <span>]</span> <span>&amp;&amp;</span> <span>[</span> <span>&#34;</span><span>$json_response</span><span>&#34;</span> !<span>=</span> <span>&#34;null&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>    <span># Parse the JSON response - check if jq is still available</span>
</span></span><span><span>    <span>if</span> <span>command</span> -v jq &gt;/dev/null 2&gt;<span>&amp;</span>1<span>;</span> <span>then</span>
</span></span><span><span>        <span>is_spam</span><span>=</span><span>$(</span><span>echo</span> <span>&#34;</span><span>$json_response</span><span>&#34;</span> <span>|</span> jq -r <span>&#39;.is_spam&#39;</span> 2&gt;/dev/null<span>)</span>
</span></span><span><span>        <span>confidence</span><span>=</span><span>$(</span><span>echo</span> <span>&#34;</span><span>$json_response</span><span>&#34;</span> <span>|</span> jq -r <span>&#39;.confidence&#39;</span> 2&gt;/dev/null<span>)</span>
</span></span><span><span>        <span>reason</span><span>=</span><span>$(</span><span>echo</span> <span>&#34;</span><span>$json_response</span><span>&#34;</span> <span>|</span> jq -r <span>&#39;.reason&#39;</span> 2&gt;/dev/null<span>)</span>
</span></span><span><span>    <span>else</span>
</span></span><span><span>        log <span>&#34;ERROR: jq command not found during JSON response parsing - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>&#34;</span>
</span></span><span><span>        <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;OK&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>        <span>exit</span> <span>0</span>
</span></span><span><span>    <span>fi</span>
</span></span><span><span>    
</span></span><span><span>    <span>if</span> <span>[</span> <span>&#34;</span><span>$is_spam</span><span>&#34;</span> <span>=</span> <span>&#34;true&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>        log <span>&#34;SPAM DETECTED - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>, Confidence: </span><span>$confidence</span><span>, Reason: </span><span>$reason</span><span>, OpenRouter Analysis: </span><span>$openrouter_analysis</span><span>&#34;</span>
</span></span><span><span>        
</span></span><span><span>        <span># Calculate score from confidence</span>
</span></span><span><span>        <span>score</span><span>=</span><span>$(</span>awk <span>&#34;BEGIN {printf \&#34;%.0f\&#34;, </span><span>$confidence</span><span> * 10}&#34;</span><span>)</span>
</span></span><span><span>        
</span></span><span><span>        <span># Extract domain from sender email address</span>
</span></span><span><span>        <span>domain</span><span>=</span><span>$(</span><span>echo</span> <span>&#34;</span><span>$sender</span><span>&#34;</span> <span>|</span> grep -oE <span>&#39;@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}&#39;</span> <span>|</span> sed <span>&#39;s/^@//&#39;</span> <span>|</span> head -n 1<span>)</span>
</span></span><span><span>        <span>if</span> <span>[</span> -n <span>&#34;</span><span>$domain</span><span>&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>            <span>blacklist_file</span><span>=</span><span>&#34;</span><span>$(</span>dirname <span>&#34;</span><span>$0</span><span>&#34;</span><span>)</span><span>/blacklist.txt&#34;</span>
</span></span><span><span>            <span># Check if domain is already in blacklist before adding</span>
</span></span><span><span>            <span>if</span> <span>[</span> ! -f <span>&#34;</span><span>$blacklist_file</span><span>&#34;</span> <span>]</span> <span>||</span> ! grep -Fxq <span>&#34;</span><span>$domain</span><span>&#34;</span> <span>&#34;</span><span>$blacklist_file</span><span>&#34;</span><span>;</span> <span>then</span>
</span></span><span><span>                log <span>&#34;Confirmed spam detected, adding domain </span><span>$domain</span><span> to blacklist&#34;</span>
</span></span><span><span>                <span># Try to add to blacklist, but ignore errors</span>
</span></span><span><span>                <span>{</span>
</span></span><span><span>                    <span>echo</span> <span>&#34;</span><span>$domain</span><span>&#34;</span> &gt;&gt; <span>&#34;</span><span>$blacklist_file</span><span>&#34;</span>
</span></span><span><span>                <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>            <span>fi</span>
</span></span><span><span>        <span>fi</span>
</span></span><span><span>        
</span></span><span><span>        <span># Output the score result and exit immediately</span>
</span></span><span><span>        <span>{</span>
</span></span><span><span>            <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;SCORE: </span><span>$score</span><span>&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>        <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>        <span>exit</span> <span>0</span>
</span></span><span><span>    <span>elif</span> <span>[</span> -n <span>&#34;</span><span>$confidence</span><span>&#34;</span> <span>]</span> <span>&amp;&amp;</span> <span>[</span> <span>&#34;</span><span>$confidence</span><span>&#34;</span> !<span>=</span> <span>&#34;null&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>        <span># Check if awk is available for score calculation</span>
</span></span><span><span>        <span>if</span> ! <span>command</span> -v awk &gt;/dev/null 2&gt;<span>&amp;</span>1<span>;</span> <span>then</span>
</span></span><span><span>            log <span>&#34;ERROR: awk command not found - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>&#34;</span>
</span></span><span><span>            <span>{</span>
</span></span><span><span>                <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;OK&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>            <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>            <span>exit</span> <span>0</span>
</span></span><span><span>        <span>fi</span>
</span></span><span><span>        
</span></span><span><span>        <span># Convert confidence to a spam score between 0 and 10 using awk instead of bc</span>
</span></span><span><span>        <span>score</span><span>=</span><span>$(</span>awk <span>&#34;BEGIN {printf \&#34;%.0f\&#34;, </span><span>$confidence</span><span> * 10}&#34;</span><span>)</span>
</span></span><span><span>        log <span>&#34;Spam analysis completed - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>, Confidence: </span><span>$confidence</span><span>, Score: </span><span>$score</span><span>, OpenRouter Analysis: </span><span>$openrouter_analysis</span><span>&#34;</span>
</span></span><span><span>
</span></span><span><span>        <span># Only add score if it&#39;s meaningful (at least 0.3 confidence)</span>
</span></span><span><span>        <span>if</span> <span>((</span> <span>$(</span>awk <span>&#34;BEGIN {print (</span><span>$confidence</span><span> &gt; 0.3)}&#34;</span><span>)</span> <span>))</span><span>;</span> <span>then</span>
</span></span><span><span>            <span># If score is 7 or higher, add domain to blacklist</span>
</span></span><span><span>            <span>if</span> <span>[</span> <span>&#34;</span><span>$score</span><span>&#34;</span> -ge <span>7</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>                <span># Extract domain from sender email address</span>
</span></span><span><span>                <span>domain</span><span>=</span><span>$(</span><span>echo</span> <span>&#34;</span><span>$sender</span><span>&#34;</span> <span>|</span> grep -oE <span>&#39;@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}&#39;</span> <span>|</span> sed <span>&#39;s/^@//&#39;</span> <span>|</span> head -n 1<span>)</span>
</span></span><span><span>                <span>if</span> <span>[</span> -n <span>&#34;</span><span>$domain</span><span>&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>                    <span>blacklist_file</span><span>=</span><span>&#34;</span><span>$(</span>dirname <span>&#34;</span><span>$0</span><span>&#34;</span><span>)</span><span>/blacklist.txt&#34;</span>
</span></span><span><span>                    <span># Check if domain is already in blacklist before adding</span>
</span></span><span><span>                    <span>if</span> <span>[</span> ! -f <span>&#34;</span><span>$blacklist_file</span><span>&#34;</span> <span>]</span> <span>||</span> ! grep -Fxq <span>&#34;</span><span>$domain</span><span>&#34;</span> <span>&#34;</span><span>$blacklist_file</span><span>&#34;</span><span>;</span> <span>then</span>
</span></span><span><span>                        log <span>&#34;Score </span><span>$score</span><span> &gt;= 7, adding domain </span><span>$domain</span><span> to blacklist&#34;</span>
</span></span><span><span>                        <span># Try to add to blacklist, but ignore errors</span>
</span></span><span><span>                        <span>{</span>
</span></span><span><span>                            <span>echo</span> <span>&#34;</span><span>$domain</span><span>&#34;</span> &gt;&gt; <span>&#34;</span><span>$blacklist_file</span><span>&#34;</span>
</span></span><span><span>                        <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>                    <span>fi</span>
</span></span><span><span>                <span>fi</span>
</span></span><span><span>            <span>fi</span>
</span></span><span><span>            <span># Output the score result and exit immediately</span>
</span></span><span><span>            <span>{</span>
</span></span><span><span>                <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;SCORE: </span><span>$score</span><span>&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>            <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>            <span>exit</span> <span>0</span>
</span></span><span><span>        <span>else</span>
</span></span><span><span>            <span># Output OK result and exit immediately</span>
</span></span><span><span>            <span>{</span>
</span></span><span><span>                <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;OK&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>            <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>            <span>exit</span> <span>0</span>
</span></span><span><span>        <span>fi</span>
</span></span><span><span>    <span>fi</span>
</span></span><span><span><span>else</span>
</span></span><span><span>    <span># If JSON parsing fails, try to infer from plain text response</span>
</span></span><span><span>    <span># Check if grep is available before using it</span>
</span></span><span><span>    <span>if</span> ! <span>command</span> -v grep &gt;/dev/null 2&gt;<span>&amp;</span>1<span>;</span> <span>then</span>
</span></span><span><span>        log <span>&#34;ERROR: grep command not found - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>&#34;</span>
</span></span><span><span>        <span>{</span>
</span></span><span><span>            <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;OK&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>        <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>        <span>exit</span> <span>0</span>
</span></span><span><span>    <span>fi</span>
</span></span><span><span>    
</span></span><span><span>    <span>if</span> <span>echo</span> <span>&#34;</span><span>$ai_response</span><span>&#34;</span> <span>|</span> grep -qi <span>&#34;spam\|phishing\|malware\|virus\|suspicious&#34;</span><span>;</span> <span>then</span>
</span></span><span><span>        log <span>&#34;SPAM DETECTED in plain text analysis - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>, Analysis: </span><span>$ai_response</span><span>, OpenRouter Analysis: </span><span>$openrouter_analysis</span><span>&#34;</span>
</span></span><span><span>        
</span></span><span><span>        <span># Extract domain from sender email address</span>
</span></span><span><span>        <span>domain</span><span>=</span><span>$(</span><span>echo</span> <span>&#34;</span><span>$sender</span><span>&#34;</span> <span>|</span> grep -oE <span>&#39;@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}&#39;</span> <span>|</span> sed <span>&#39;s/^@//&#39;</span> <span>|</span> head -n 1<span>)</span>
</span></span><span><span>        <span>if</span> <span>[</span> -n <span>&#34;</span><span>$domain</span><span>&#34;</span> <span>]</span><span>;</span> <span>then</span>
</span></span><span><span>            <span>blacklist_file</span><span>=</span><span>&#34;</span><span>$(</span>dirname <span>&#34;</span><span>$0</span><span>&#34;</span><span>)</span><span>/blacklist.txt&#34;</span>
</span></span><span><span>            <span># Check if domain is already in blacklist before adding</span>
</span></span><span><span>            <span>if</span> <span>[</span> ! -f <span>&#34;</span><span>$blacklist_file</span><span>&#34;</span> <span>]</span> <span>||</span> ! grep -Fxq <span>&#34;</span><span>$domain</span><span>&#34;</span> <span>&#34;</span><span>$blacklist_file</span><span>&#34;</span><span>;</span> <span>then</span>
</span></span><span><span>                log <span>&#34;High confidence spam detected, adding domain </span><span>$domain</span><span> to blacklist&#34;</span>
</span></span><span><span>                <span># Try to add to blacklist, but ignore errors</span>
</span></span><span><span>                <span>{</span>
</span></span><span><span>                    <span>echo</span> <span>&#34;</span><span>$domain</span><span>&#34;</span> &gt;&gt; <span>&#34;</span><span>$blacklist_file</span><span>&#34;</span>
</span></span><span><span>                <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>            <span>fi</span>
</span></span><span><span>        <span>fi</span>
</span></span><span><span>        
</span></span><span><span>        <span># Output the score result and exit immediately</span>
</span></span><span><span>        <span>{</span>
</span></span><span><span>            <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;SCORE: 10&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span>        <span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span>        <span>exit</span> <span>0</span>
</span></span><span><span>    <span>fi</span>
</span></span><span><span><span>fi</span>
</span></span><span><span>
</span></span><span><span><span># Final log and result output - make sure nothing in this section fails</span>
</span></span><span><span>log <span>&#34;Email passed spam check - Sender: </span><span>$sender</span><span>, Receiver: </span><span>$receiver</span><span>, OpenRouter Analysis: </span><span>$openrouter_analysis</span><span>&#34;</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span><span>{</span>
</span></span><span><span>    <span>printf</span> <span>&#39;%s\n&#39;</span> <span>&#34;OK&#34;</span> &gt;<span>&amp;</span><span>1</span>
</span></span><span><span><span>}</span> 2&gt;/dev/null <span>||</span> <span>true</span>
</span></span><span><span><span>exit</span> <span>0</span>
</span></span></code></pre></div>

<h2>Дополнения 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B4%D0%BE%D0%BF%D0%BE%D0%BB%D0%BD%D0%B5%D0%BD%D0%B8%D1%8F" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Скрипт должен находится по адресу <code>/usr/local/bin/pmg-custom-check</code>, в этой же папке <code>whitelist.txt</code> и <code>blacklist.txt</code></p>
<p>А в <code>/etc/pmg/pmg.conf</code> должен быть включен функционал <code>cutsom check</code></p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>section: admin
</span></span><span><span>    custom_check <span>1</span>
</span></span></code></pre></div><p>И чтобы два раза не вставать, в PMG у меня уже есть какие-то домены в whitelist|blacklist, я решил их сразу добавить сюда.
Сначала воспользуемся командой</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo pmgsh get /config/ruledb/who
</span></span></code></pre></div><p>чтобы определить ID, созданных в PMG whitelist|blacklist, затем экспортируем домены во временные файлы</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo pmgsh get /config/ruledb/who/2/objects <span>|</span> jq <span>&#39;.[] | select(.domain != null) | .domain&#39;</span> &gt; /tmp/blacklist.text
</span></span><span><span>sudo pmgsh get /config/ruledb/who/3/objects <span>|</span> jq <span>&#39;.[] | select(.domain != null) | .domain&#39;</span> &gt; /tmp/whitelist.txt
</span></span></code></pre></div><p>После чего можем добавить их в <code>/usr/local/bin/</code></p>


<h2>Итог 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B8%D1%82%D0%BE%D0%B3" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Прикрутил AI к проверке почты. Я использовал OpenRouter и модель mistralai/mistral-small-3.1-24b-instruct (достаточно бюджетная). Сейчас я использую этот скрипт на не очень критичной почте, не уверен, что готов отправлять нейронке все свои сообщения. Можно использовать и какие-нибудь локальные llama-модели, но подходящего железа под рукой нет, а протестировать концепцию использования AI для анализа спама хотелось.</p>
]]></content:encoded></item><item><title>Первые 5 минут на сервере</title><link>https://andrey.dvur.me/posts/2024-09-09-5-minutes-vps/</link><guid isPermaLink="true">https://andrey.dvur.me/posts/2024-09-09-5-minutes-vps/</guid><pubDate>Mon, 09 Sep 2024 00:00:00 +0000</pubDate><category>format-article</category><category>index</category><category>comment-all</category><enclosure url="https://andrey.dvur.me/posts/2024-09-09-5-minutes-vps/featured.jpg" type="image/jpeg"/><content:encoded><![CDATA[<p>Купили VPS, получили от провайдера пароль root и ip-адрес.
А дальше что?</p>


<h2>Первым делом 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%BF%D0%B5%D1%80%D0%B2%D1%8B%D0%BC-%D0%B4%D0%B5%D0%BB%D0%BE%D0%BC" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Заходим на сервер <em>первый и последний</em> раз под рутом</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>ssh root@ip
</span></span></code></pre></div><p>Обновляемся</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>apt update <span>&amp;&amp;</span> apt upgrade -y
</span></span></code></pre></div><p>Устанавливаем минимально нужное ПО</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>apt install sudo ufw nano wget fish neofetch ncdu btop fail2ban
</span></span></code></pre></div><p>Создаем пользователя</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>useradd -m -G sudo -s /usr/bin/fish odin <span>&amp;&amp;</span> passwd odin
</span></span></code></pre></div>
  



<div
  
   
  >

  <span
    
     
    >

    

  <span>
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z"/></svg>

  </span>


  </span>

  <span
    
     
    >Здесь я для пользователя указываю оболочку fish-shell, к которой я привык</span>
</div>

<p>Проверяем сетевые соединения</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>ss -tulpan
</span></span></code></pre></div><p>На этом этапе ожидаем увидеть только наше соединение на 22 порту.

  



<div
  
   
  >

  <span
    
     
    >

    

  <span>
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z"/></svg>

  </span>


  </span>

  <span
    
     
    >Если соединений больше - это повод уже на этом этапе озаботится что именно и куда подключается с нашего девственного сервера.</span>
</div>
</p>


<h2>Сделаем сервер чуть безопаснее 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D1%81%D0%B4%D0%B5%D0%BB%D0%B0%D0%B5%D0%BC-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D1%87%D1%83%D1%82%D1%8C-%D0%B1%D0%B5%D0%B7%D0%BE%D0%BF%D0%B0%D1%81%D0%BD%D0%B5%D0%B5" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Отключаем возможность логиниться под рутом</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>nano /etc/ssh/sshd_config
</span></span></code></pre></div><p>Меняем значение параметра <code>PermitRootLogin</code> указываем <code>no</code></p>
<p>Настроим файервол, разрешив любые подключения только со своего ip-адреса</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>ufw allow from 1.2.3.4
</span></span></code></pre></div><blockquote>
<p>1.2.3.4 - ваш <strong>статический</strong> ip, с которого подключаетесь <strong>к</strong> VPS
{: .prompt-info }
Активируем файервол</p>
</blockquote>
<div><pre tabindex="0"><code data-lang="bash"><span><span>ufw <span>enable</span>
</span></span></code></pre></div><p>И проверяем</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>ufw status verbose
</span></span></code></pre></div><p>Должны увидеть статус <code>active</code> и соединения разрешены любые <code>Anywhere</code> с нашего ip-адреса <code>1.2.3.4</code></p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>Status: active
</span></span><span><span>Logging: on <span>(</span>low<span>)</span>
</span></span><span><span>Default: deny <span>(</span>incoming<span>)</span>, allow <span>(</span>outgoing<span>)</span>, deny <span>(</span>routed<span>)</span>
</span></span><span><span>New profiles: skip
</span></span><span><span>
</span></span><span><span>To                         Action      From
</span></span><span><span>--                         ------      ----
</span></span><span><span>Anywhere                   ALLOW IN    1.2.3.4   
</span></span></code></pre></div><p>Завершаем сессию под рутом <code>exit</code> или <code>Cntrl+d</code> и заходим под созданным ранее пользователем</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>ssh odin@ip
</span></span></code></pre></div><p>Добавим авторизацию по ключам</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>mkdir .ssh
</span></span><span><span>nano .ssh/authorized_keys
</span></span></code></pre></div><p>Добавляем в файл <code>authorized_keys</code> наши <strong>публичные</strong> ключи</p>
<p>Завершаем сессию <code>exit</code> или <code>Cntrl+d</code> и заходим еще раз</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>ssh odin@ip
</span></span></code></pre></div><p>В этот раз вход должен пройти без запроса пароля</p>

  



<div
  
   
  >

  <span
    
     
    >

    

  <span>
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z"/></svg>

  </span>


  </span>

  <span
    
     
    >Интереса ради можем проверить, кто пытался зайти на наш сервер, пока мы его настраивали</span>
</div>

<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo grep <span>&#34;Failed password&#34;</span> /var/log/auth.log <span>|</span> cut -d <span>&#39; &#39;</span> -f <span>9</span> <span>|</span> sort <span>|</span> uniq -c <span>|</span> sort -rh
</span></span><span><span>sudo grep <span>&#34;Invalid user&#34;</span> /var/log/auth.log <span>|</span> cut -d <span>&#39; &#39;</span> -f <span>9</span> <span>|</span> sort <span>|</span> uniq -c <span>|</span> sort -rh
</span></span></code></pre></div><p>Перезагружаемся</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo reboot
</span></span></code></pre></div><p>Базовая настройка выполнена</p>


<h2>Второстепенные настройки 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B2%D1%82%D0%BE%D1%80%D0%BE%D1%81%D1%82%D0%B5%D0%BF%D0%B5%D0%BD%D0%BD%D1%8B%D0%B5-%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B8" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Настройка UTF-8 локали</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo dpkg-reconfigure locales
</span></span></code></pre></div><div><pre tabindex="0"><code data-lang="bash"><span><span><span>export</span> <span>LC_ALL</span><span>=</span>en_US.UTF-8
</span></span><span><span><span>export</span> <span>LANG</span><span>=</span>en_US.UTF-8
</span></span><span><span><span>export</span> <span>LANGUAGE</span><span>=</span>en_US.UTF-8
</span></span></code></pre></div><p>Меняем имя нашего сервера</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo nano /etc/hostname
</span></span></code></pre></div><p>Настраиваем часовой пояс</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo timedatectl set-timezone Europe/Moscow
</span></span></code></pre></div><p>Еще у нас установлены дополнительные утилиты, у каждой есть свои настройки, но можно пользоваться и параметрами по-умолчанию</p>
<ul>
<li><code>neofetch</code> или более современный <code>fastfetch</code> - отображение системный информации</li>
<li><code>btop</code> - монитор системных ресурсов</li>
<li><code>fail2ban</code> - автобан тех ip-адресов, кто пытается подбирать пароли</li>
<li><code>ncdu</code> - отображение размера папок</li>
</ul>


<h2>Итог 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B8%D1%82%D0%BE%D0%B3" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<ul>
<li>создали пользователя и отключили root</li>
<li>установили и настроили файервол, разрешив подключения пока только со своего ip-адреса</li>
<li>настроили авторизацию по ключам</li>
<li>настроили локаль, имя сервера и часовой пояс</li>
<li>установили дополнительные утилиты</li>
</ul>
]]></content:encoded></item><item><title>Смотрим торренты на AppleTV</title><link>https://andrey.dvur.me/posts/2024-03-12-torrserver/</link><guid isPermaLink="true">https://andrey.dvur.me/posts/2024-03-12-torrserver/</guid><pubDate>Tue, 12 Mar 2024 00:00:00 +0000</pubDate><category>format-article</category><category>index</category><category>comment-all</category><enclosure url="https://andrey.dvur.me/posts/2024-03-12-torrserver/featured.jpeg" type="image/jpeg"/><content:encoded><![CDATA[

<h2>Дано 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B4%D0%B0%D0%BD%D0%BE" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Что такое торренты знают все, как их смотреть на андроид приставках знают многие.
Расскажу как смотреть торренты на AppleTV без предварительного скачивания,  регистрации и смс.</p>


<h2>Приступим 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%BF%D1%80%D0%B8%D1%81%D1%82%D1%83%D0%BF%D0%B8%D0%BC" aria-label="Якорь">#</a>
    </span>        
    
</h2>


<h3>1 - TorrServer 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#1---torrserver" aria-label="Якорь">#</a>
    </span>        
    
</h3>
<p>Нам нужен кеширующий торрент сервер, который будет докачивать файл в то время, как мы его уже смотрим.
В этой роли выступает <a href="https://github.com/YouROK/TorrServer" target="_blank">TorrServer</a>
Запускаем его на отдельном сервере/компьютере с помощью docker</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>docker run --rm -d -e <span>TS_PORT</span><span>=</span><span>5665</span> -e <span>TS_DONTKILL</span><span>=</span><span>1</span> -e <span>TS_HTTPAUTH</span><span>=</span><span>1</span> -e <span>TS_RDB</span><span>=</span><span>1</span> -e <span>TS_CONF_PATH</span><span>=</span>/opt/ts/config -e <span>TS_LOG_PATH</span><span>=</span>/opt/ts/log -e <span>TS_TORR_DIR</span><span>=</span>/opt/ts/torrents --name torrserver -v ~/ts:/opt/ts -p 5665:5665 ghcr.io/yourok/torrserver:latest
</span></span></code></pre></div><p>или docker compose</p>
<div><pre tabindex="0"><code data-lang="yaml"><span><span><span># docker-compose.yml</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span></span><span>version</span><span>:</span><span> </span><span>&#39;3.3&#39;</span><span>
</span></span></span><span><span><span></span><span>services</span><span>:</span><span>
</span></span></span><span><span><span>    </span><span>torrserver</span><span>:</span><span>
</span></span></span><span><span><span>        </span><span>image</span><span>:</span><span> </span><span>ghcr.io/yourok/torrserver</span><span>
</span></span></span><span><span><span>        </span><span>container_name</span><span>:</span><span> </span><span>torrserver</span><span>
</span></span></span><span><span><span>        </span><span>environment</span><span>:</span><span>
</span></span></span><span><span><span>            </span>- <span>TS_PORT=5665</span><span>
</span></span></span><span><span><span>            </span>- <span>TS_DONTKILL=1</span><span>
</span></span></span><span><span><span>            </span>- <span>TS_HTTPAUTH=0</span><span>
</span></span></span><span><span><span>            </span>- <span>TS_CONF_PATH=/opt/ts/config</span><span>
</span></span></span><span><span><span>            </span>- <span>TS_TORR_DIR=/opt/ts/torrents</span><span>
</span></span></span><span><span><span>        </span><span>volumes</span><span>:</span><span>
</span></span></span><span><span><span>            </span>- <span>&#39;./CACHE:/opt/ts/torrents&#39;</span><span>
</span></span></span><span><span><span>            </span>- <span>&#39;./CONFIG:/opt/ts/config&#39;</span><span>
</span></span></span><span><span><span>        </span><span>ports</span><span>:</span><span>
</span></span></span><span><span><span>            </span>- <span>&#39;5665:5665&#39;</span><span>
</span></span></span><span><span><span>        </span><span>restart</span><span>:</span><span> </span><span>unless-stopped</span><span>
</span></span></span></code></pre></div><p>Здесь <code>CONFIG</code> директория, где сохраняется файл конфигурации (создается по-умолчанию, редактировать его необязательно), <code>CACHE</code> - папка, куда будут временно скачиваться торренты, пока мы их смотрим.</p>


<h3>2 - LAMPA 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#2---lampa" aria-label="Якорь">#</a>
    </span>        
    
</h3>
<p>Приложение для AppleTV называется <a href="http://lampa.mx/" target="_blank">LAMPA</a> (можно смореть и в браузере на компьютере)</p>
<p>Приложения на данный момент нет в AppStore <em>и вряд ли будет</em>. Поэтому качаем его через <a href="https://apps.apple.com/ru/app/testflight/id899247664" target="_blank">TestFlight</a>.
TestFlight должен быть установлен и на телефоне и на AppleTV, оба должны быть привязаны к одному Apple ID.</p>
<ul>
<li>Сначала на <strong>телефоне</strong> открываем <a href="https://testflight.apple.com/join/4xqg1q15" target="_blank">ссылку на тестирование LAMPA</a></li>
<li>Открыть TestFlight <strong>на Apple TV</strong>, будет виден список из приложений, где вы участвуете в тестировании</li>
<li>Принять, Установить, открыть.</li>
</ul>


<h3>3 - Настройки 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#3---%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B8" aria-label="Якорь">#</a>
    </span>        
    
</h3>
<p>Сразу после установки LAMPA на Apple TV максимум, что она нам предложит - это посмотреть трейлеры к фильмам, потому что функцию торрентов из нее вырезали (видимо иначе не пройдет модерацию).
Возвращаем функционал просмотра торрентов, для этого установим <a href="http://cub.red/plugin/etor" target="_blank">плагин</a> - Настройки -&gt; Расширения -&gt; Добавить плагин.</p>
<p>После этого в настройках появятся два новых пункта <code>Парсер</code> и <code>Torrserver</code>.
В меню <code>Torrserver</code> <em>основная ссылка</em> указываем адрес компьютера и порт, где у нас запущен torrserer - <em>что-то вроде</em> http://192.168.88.10:5665</p>
<p>В меню <code>Парсер</code> указываем - тип парсера <code>jackett</code>, ссылка <code>jacred.xyz</code>, все остальные поля пропускаем.</p>
<p>А еще для воспроизведения торрентов нам нужен сторонний плеер <code>Infuse</code> или <code>VLC</code>, скачиваем их в App Store и указываем в лампе Настройка -&gt; Плеер -&gt; Тип плеера для торрентов.</p>


<h2>Результат 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D1%80%D0%B5%D0%B7%D1%83%D0%BB%D1%8C%D1%82%D0%B0%D1%82" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>При нажатии <code>Смотреть</code> появится опция <em>торрент</em> и дальше выбираем из списка, какой вариант для просмотра нам больше подходит</p>


<h2>Итог 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B8%D1%82%D0%BE%D0%B3" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Не самое простое решение, не самое удобное - зато <strong>абсолютно бесплатное</strong></p>
]]></content:encoded></item><item><title>Превращаем андроид ТВ приставку в сервер</title><link>https://andrey.dvur.me/posts/2023-11-01-tvbox/</link><guid isPermaLink="true">https://andrey.dvur.me/posts/2023-11-01-tvbox/</guid><pubDate>Wed, 01 Nov 2023 00:00:00 +0000</pubDate><category>format-article</category><category>index</category><category>comment-all</category><enclosure url="https://andrey.dvur.me/posts/2023-11-01-tvbox/featured.jpg" type="image/jpeg"/><content:encoded><![CDATA[

<h2>Дано 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B4%D0%B0%D0%BD%D0%BE" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>ТВ приставка, купленная когда-то для просмотра 4k фильмов, не подошла она по разным причинам и отправилась в чулан на несколько лет.</p>


<h2>Приступим 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%BF%D1%80%D0%B8%D1%81%D1%82%D1%83%D0%BF%D0%B8%D0%BC" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>По надписи на приставке <code>Mini M8S</code> находится информация, что приставка на чипсете <code>S905</code>. А вот при загрузке андроида на экране отображается эмблема <code>S905X</code>.</p>
<ul>
<li>Скачиваем образ armbian с <a href="https://github.com/ophub/amlogic-s9xxx-armbian/releases/" target="_blank">гитхаба</a> для чипа <code>S905X</code> и записываем на microSD c помощью программы  <a href="https://etcher.balena.io/" target="_blank">Balena</a></li>
<li>На microSD появится два раздела: <code>BOOT</code> и <code>ROOTFS</code>, переходим в раздел <code>BOOT</code>
<ul>
<li>Находим файл, подходящий для нашей архитектуры <code>u-boot-s905x-s912.bin</code> и переименовываем его в <code>u-boot.ext</code></li>
<li>В папке <code>extlinux</code> проверяем файл <code>extlinux.conf</code>. В моем случае редактировать содержимое файла не пришлось, но сам файл назывался неверно - <code>extlinux.conf.bak</code></li>
</ul>
</li>
<li>Вытаскиваем флешку из компьютера и вставляем в ТВ приставку, вынимаем шнур питания, зубочисткой зажимаем кнопку <code>reset</code> и вставляем шнур питания обратно</li>
</ul>


<h2>ТА-ДАА 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D1%82%D0%B0-%D0%B4%D0%B0%D0%B0" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-11-01-tvbox/armbian_hu_4ab5260610f70da3.jpg 330w,
        /posts/2023-11-01-tvbox/armbian_hu_80ce578acc22944a.jpg 660w,
        /posts/2023-11-01-tvbox/armbian_hu_866b73e72ce28269.jpg 1024w,
        /posts/2023-11-01-tvbox/armbian_hu_404a8878bc2df15c.jpg 2x"
        data-zoom-src="/posts/2023-11-01-tvbox/armbian_hu_404a8878bc2df15c.jpg"
        src="/posts/2023-11-01-tvbox/armbian_hu_80ce578acc22944a.jpg"
        alt=""
      />
      
    </figure>
</p>
<p>Загрузилась вариация Ubuntu Lunar для arm-процессоров.</p>
<p>Работает наш линукс пока еще с флешкарты, сколько карточка проживет в таком режиме неизвестно, поэтому установим систему на внутреннюю память приставки.</p>
<blockquote>
<p>ВНИМАНИЕ: андроид на приставке вы таким образом уничтожите, как и все содержимое на внутренем носителе, обратного пути не будет.
{: .prompt-danger }</p>
</blockquote>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo armbian-install
</span></span></code></pre></div><p>Первая попытка заканчивается ошибкой - система не может найти файл <code>u-boot-s905x-s912.bin</code>. Естественно - мы же ранее переименовали его в <code>u-boot.ext</code>. Поэтому копируем его с оригинальным названием</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo cp /boot/u-boot.ext /boot/u-boot-s905x-s912.bin
</span></span></code></pre></div><p>И снова запускаем команду <code>armbian-install</code>. В этот раз установка системы проходит без ошибок и предлагает перезагрузиться и вытащить флешку. Так и делаем.</p>


<h2>Цель 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D1%86%D0%B5%D0%BB%D1%8C" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>После загрузки и проверки, что линукс работает и грузится без проблем, приступим собственно к каким-нибудь полезным вещам.</p>


<h3>AdGuard 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#adguard" aria-label="Якорь">#</a>
    </span>        
    
</h3>
<p>В предыдущей <a href="https://andrey.dvur.me/posts/mikrotikdocker/" target="_blank">статье</a> я пробовал установить Adguard на роутер Mikrotik, получилось не очень рабочее решение. Попробуем теперь приставку превратить в dns-сервер Adguard.</p>
<p><a href="https://github.com/AdguardTeam/AdGuardHome" target="_blank">AdGuardHome</a> устанавливается достаточно просто, автоматическим скриптом.</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>wget --no-verbose -O - https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh <span>|</span> sh -s -- -v
</span></span></code></pre></div><p>В роутере необходимо указать, что для локальной сети в качестве dns-сервера будет использоваться наша приставка.</p>
<blockquote>
<p>Приставке потребуется статический адрес
{: .prompt-tip }</p>
</blockquote>
<p>В Микротике это <code>IP -&gt; DHCP server -&gt; Networks -&gt; DNS Servers</code></p>
<p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-11-01-tvbox/adguard-home_hu_f27b0cf28a526e87.jpg 330w,
        /posts/2023-11-01-tvbox/adguard-home_hu_40d600c46c9ba2c6.jpg 660w,
        /posts/2023-11-01-tvbox/adguard-home_hu_fdc30aea203bfdba.jpg 1024w,
        /posts/2023-11-01-tvbox/adguard-home_hu_5e4a979b5d0125ed.jpg 2x"
        data-zoom-src="/posts/2023-11-01-tvbox/adguard-home_hu_5e4a979b5d0125ed.jpg"
        src="/posts/2023-11-01-tvbox/adguard-home_hu_40d600c46c9ba2c6.jpg"
        alt=""
      />
      
    </figure>
</p>


<h3>Docker и умный дом 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#docker-%D0%B8-%D1%83%D0%BC%D0%BD%D1%8B%D0%B9-%D0%B4%D0%BE%D0%BC" aria-label="Якорь">#</a>
    </span>        
    
</h3>
<p>Установим докер и сервер для умного дома.
ПО на armbian можно устанавливать с помощью <code>armbian-config</code></p>
<p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-11-01-tvbox/armbian-config-1_hu_561fad1994967641.jpg 330w,
        /posts/2023-11-01-tvbox/armbian-config-1_hu_34ce7373bb4131ad.jpg 660w,
        /posts/2023-11-01-tvbox/armbian-config-1_hu_69d4dc74dd56c54e.jpg 1024w,
        /posts/2023-11-01-tvbox/armbian-config-1_hu_8ab649ccb24b9b58.jpg 2x"
        data-zoom-src="/posts/2023-11-01-tvbox/armbian-config-1_hu_8ab649ccb24b9b58.jpg"
        src="/posts/2023-11-01-tvbox/armbian-config-1_hu_34ce7373bb4131ad.jpg"
        alt=""
      />
      
    </figure>
</p>
<p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-11-01-tvbox/armbian-config-2_hu_db47052adbd2ef96.jpg 330w,
        /posts/2023-11-01-tvbox/armbian-config-2_hu_94ce1e9bcd4bc04c.jpg 660w,
        /posts/2023-11-01-tvbox/armbian-config-2_hu_7ea5399b044fb0cc.jpg 1024w,
        /posts/2023-11-01-tvbox/armbian-config-2_hu_881deb9b4fa8d464.jpg 2x"
        data-zoom-src="/posts/2023-11-01-tvbox/armbian-config-2_hu_881deb9b4fa8d464.jpg"
        src="/posts/2023-11-01-tvbox/armbian-config-2_hu_94ce1e9bcd4bc04c.jpg"
        alt=""
      />
      
    </figure>
</p>
<p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-11-01-tvbox/armbian-config-3_hu_b9f7a31dcf75f39d.jpg 330w,
        /posts/2023-11-01-tvbox/armbian-config-3_hu_bd1ed236e45d8142.jpg 660w,
        /posts/2023-11-01-tvbox/armbian-config-3_hu_4dd138bf02fb5175.jpg 1024w,
        /posts/2023-11-01-tvbox/armbian-config-3_hu_1a501738516aca5.jpg 2x"
        data-zoom-src="/posts/2023-11-01-tvbox/armbian-config-3_hu_1a501738516aca5.jpg"
        src="/posts/2023-11-01-tvbox/armbian-config-3_hu_bd1ed236e45d8142.jpg"
        alt=""
      />
      
    </figure>
</p>
<p><strong>НО</strong> Openhab таким образом не установился, докер установился, но отказался запускаться.
Поэтому ставим все <em>ручками</em>.</p>
<p><a href="https://docs.docker.com/engine/install/ubuntu/" target="_blank">Инструкция для докер.</a></p>
<p><a href="https://www.openhab.org/download/" target="_blank">Инструкция для Openhab.</a></p>
<p>В дополнение еще поставил:</p>
<ul>
<li><a href="https://homebridge.io/" target="_blank">Homebridge</a> для интеграции openhab и умного дома Apple</li>
<li><a href="https://www.portainer.io/" target="_blank">Portainer</a> для удобного управления докер-контейнерами</li>
<li><a href="https://containrrr.dev/watchtower/" target="_blank">Watchtower</a> чтоб контейнеры сами обновлялись</li>
<li><a href="https://github.com/louislam/uptime-kuma" target="_blank">Uptime Kuma</a> - мониторит состояние моих внутренних/внешних сервисов и шлет мне сообщения в телеграм, если что-то сломалось</li>
</ul>
<p>Все вместе потребляет примерно 60% оперативной памяти

    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-11-01-tvbox/arm-neofetch_hu_d72ee4f3b9c5c79c.jpg 330w,
        /posts/2023-11-01-tvbox/arm-neofetch_hu_3725b3b5b5c7eebc.jpg 660w,
        /posts/2023-11-01-tvbox/arm-neofetch_hu_799cdc4a926e8710.jpg 1024w,
        /posts/2023-11-01-tvbox/arm-neofetch_hu_ea712b6c7ad64d2.jpg 2x"
        data-zoom-src="/posts/2023-11-01-tvbox/arm-neofetch_hu_ea712b6c7ad64d2.jpg"
        src="/posts/2023-11-01-tvbox/arm-neofetch_hu_3725b3b5b5c7eebc.jpg"
        alt=""
      />
      
    </figure>
</p>


<h2>Итог 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B8%D1%82%D0%BE%D0%B3" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Сервер из ТВ-приставки - отличная замена решениям на малинках/апельсинках.</p>
]]></content:encoded></item><item><title>Запускаем контейнеры докер на роутере Mikrotik</title><link>https://andrey.dvur.me/posts/2023-10-23-mikrotikdocker/</link><guid isPermaLink="true">https://andrey.dvur.me/posts/2023-10-23-mikrotikdocker/</guid><pubDate>Mon, 23 Oct 2023 00:00:00 +0000</pubDate><category>format-article</category><category>index</category><category>comment-all</category><enclosure url="https://andrey.dvur.me/posts/2023-10-23-mikrotikdocker/featured.jpg" type="image/jpeg"/><content:encoded><![CDATA[

<h2>Mikrotik с версии RouterOS 7.5 поддерживает контейнеры докер 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#mikrotik-%D1%81-%D0%B2%D0%B5%D1%80%D1%81%D0%B8%D0%B8-routeros-75-%D0%BF%D0%BE%D0%B4%D0%B4%D0%B5%D1%80%D0%B6%D0%B8%D0%B2%D0%B0%D0%B5%D1%82-%D0%BA%D0%BE%D0%BD%D1%82%D0%B5%D0%B9%D0%BD%D0%B5%D1%80%D1%8B-%D0%B4%D0%BE%D0%BA%D0%B5%D1%80" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Запустим что-нибудь интересное, например AdguardHome</p>
<p>Для начала скачиваем файл <code>Extra packages</code> для своей архитектуры с <a href="https://mikrotik.com/download" target="_blank">сайта Mikrotik</a>.</p>
<p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-10-23-mikrotikdocker/mikrotik-site_hu_a2325c13807f01f2.jpg 330w,
        /posts/2023-10-23-mikrotikdocker/mikrotik-site_hu_5639900341a3d5be.jpg 660w,
        /posts/2023-10-23-mikrotikdocker/mikrotik-site_hu_efb8c94e2fd2b2c5.jpg 1024w,
        /posts/2023-10-23-mikrotikdocker/mikrotik-site_hu_e40adb19d98090da.jpg 2x"
        data-zoom-src="/posts/2023-10-23-mikrotikdocker/mikrotik-site_hu_e40adb19d98090da.jpg"
        src="/posts/2023-10-23-mikrotikdocker/mikrotik-site_hu_5639900341a3d5be.jpg"
        alt=""
      />
      
    </figure>
</p>
<p>Разархивируем и файл <code>container-7.X.npk</code> закидываем на роутер в меню <code>Files</code>.</p>
<p>Перезагружаем роутер.</p>
<p>Функционал контейнеров добавлен, но еще надо его активировать.</p>
<p>Для этого в терминале роутера выполняем команду</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>/system/device-mode/update <span>container</span><span>=</span>yes
</span></span></code></pre></div><p>После этой команды в течении 5 минут необходимо выполнить <code>cold reboot</code> роутера. <code>Сold reboot</code> - это выдернуть шнур питания из розетки или нажать кнопку <code>reset</code> на роутере. В моем случае выдергивание из розетки не помогло и пришлось вытаскивать роутер из стойки и жать кнопку <code>reset</code>.</p>
<p>Появится меню <code>container</code>:</p>
<p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-10-23-mikrotikdocker/mikrotik-containers-menu_hu_28c118fdd3bcc67b.jpg 330w,
        /posts/2023-10-23-mikrotikdocker/mikrotik-containers-menu_hu_4531dc49d05c7674.jpg 660w,
        /posts/2023-10-23-mikrotikdocker/mikrotik-containers-menu_hu_7fd09e6ddd0f044.jpg 1024w,
        /posts/2023-10-23-mikrotikdocker/mikrotik-containers-menu_hu_ae428cfbdfeff57a.jpg 2x"
        data-zoom-src="/posts/2023-10-23-mikrotikdocker/mikrotik-containers-menu_hu_ae428cfbdfeff57a.jpg"
        src="/posts/2023-10-23-mikrotikdocker/mikrotik-containers-menu_hu_4531dc49d05c7674.jpg"
        alt=""
      />
      
    </figure>
</p>
<p>В линуксе докер сам создает необходимую себе инфраструктуру: сеть, интерфейсы, адреса, правила файрвола. Здесь же придется все прописывать вручную..</p>
<p>Создадим <code>bridge</code> для контейнеров</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>/interface/bridge/add <span>name</span><span>=</span>containers
</span></span></code></pre></div><p>Назначим ему внутренний адрес</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>/ip/address/add <span>address</span><span>=</span>172.17.0.1/24 <span>interface</span><span>=</span>containers
</span></span></code></pre></div><p>Создадим виртуальный сетевой интерфейс для нашего будущего контейнера и назначим ему адрес <code>172.17.0.2</code></p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>/interface/veth/add <span>name</span><span>=</span>veth1 <span>address</span><span>=</span>172.17.0.2/24 <span>gateway</span><span>=</span>172.17.0.1
</span></span></code></pre></div><p>Добавим интерфейс будущего контейнера в созданный <code>bridge</code></p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>/interface/bridge/port add <span>bridge</span><span>=</span>containers <span>interface</span><span>=</span>veth1
</span></span></code></pre></div>
  



<div
  
   
  >

  <span
    
     
    >

    

  <span>
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z"/></svg>

  </span>


  </span>

  <span
    
     
    ><blockquote>
<p>Для каждого контейнера надо создавать свой интерфейс <code>veth</code> c новым адресом и добавлять их в <code>bridge</code></p>
</blockquote></span>
</div>

<ul>
<li></li>
</ul>

  



<div
  
   
  >

  <span
    
     
    >

    

  <span>
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z"/></svg>

  </span>


  </span>

  <span
    
     
    >Внутренней памяти роутера скорее всего окажется недостаточно для запуска большинства контейнеров, поэтому рекомендую использовать флешку или внешний диск. Их необходимо отформатировать самостоятельно, либо через меню <code>System -&gt; Disk -&gt; Format  Drive</code></span>
</div>

<p>Переходим к настройке контейнера
<code>Container -&gt; Config</code></p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>/container/config/set registry-url<span>=</span>https://registry-1.docker.io <span>tmpdir</span><span>=</span>usb1-part1/pull
</span></span></code></pre></div><p>Здесь <code>https://registry-1.docker.io</code> реестр откуда будут скачиваться образы контейнеров, <code>usb1-part1/pull</code> - директория на флешке куда они будут скачиваться.</p>
<p>Для контейнера <code>AdGuardHome</code> смонтируем с флешки две папки <code>work</code> и <code>conf</code></p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>/container/mounts/add <span>name</span><span>=</span>adguard-conf <span>src</span><span>=</span>usb1-part1/adguard/conf <span>dst</span><span>=</span>/opt/adguardhome/conf
</span></span><span><span>/container/mounts/add <span>name</span><span>=</span>adguard-work <span>src</span><span>=</span>usb1-part1/adguard/work <span>dst</span><span>=</span>/opt/adguardhome/work
</span></span></code></pre></div><p>
    <figure>
      <img loading="lazy" src="mikrotik-%20mounts.jpg" alt="" />
      
    </figure>
</p>
<p>Запускаем сам контейнер <code>AdGuardHome</code></p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>/container/add remote-image<span>=</span>adguard/adguardhome:latest <span>interface</span><span>=</span>veth1 root-dir<span>=</span>usb1-part1/adguard/root <span>mounts</span><span>=</span>adguard-conf,adguard-work
</span></span></code></pre></div><p>Ждем когда контейнер загрузится и получит статус <code>stopped</code>, после этого запускаем его кнопкой <code>Start</code> или командой</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>/container/start <span>0</span>
</span></span></code></pre></div><p>Осталось перебросить порт с роутера на вирутальный порт нашего контейнера</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>/ip firewall nat 
</span></span><span><span>add <span>action</span><span>=</span>dst-nat <span>chain</span><span>=</span>dstnat dst-address<span>=</span>192.168.88.1 dst-port<span>=</span><span>3000</span> <span>protocol</span><span>=</span>tcp to-addresses<span>=</span>172.17.0.2 to-ports<span>=</span><span>3000</span>
</span></span></code></pre></div><p>Теперь зайдя по адресу 192.168.88.1:3000 мы увидим настройку <code>AdGuardHome</code></p>
<blockquote>
<p>Советую порт, на которм будет доступна панель управления <code>AdGuardHome</code> оставить <code>3000</code>, чтобы не переписывать предыдущее правило
{: .prompt-tip }</p>
</blockquote>
<p>И укажем роутеру использовать <code>AdGuardHome</code> в качестве DNS-сервера</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>/ip dns <span>set</span> <span>servers</span><span>=</span>172.17.0.2
</span></span></code></pre></div>

<h2>Плюсы 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%BF%D0%BB%D1%8E%D1%81%D1%8B" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<ul>
<li>С помощью контейнеров можем бесконечно (в пределах аппаратных ресурсов) расширять функционал роутера</li>
<li>Можем вырезать рекламу с помощью черных списков <code>AdGuardHome</code></li>
</ul>


<h2>Минусы 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%BC%D0%B8%D0%BD%D1%83%D1%81%D1%8B" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<ul>
<li>
<p>В <code>AdGuardHome</code> мы увидим только одного клиента, собственно наш роутер с адресом <code>172.17.0.1</code>, не получится настраивать доступ к сайтам для отдельных клиентов сети</p>
</li>
<li>
<p>При перезагрузке не факт, что заработает..</p>
<p>У меня при отключении электричества, контейнеры отказались запускаться. Первый раз получилось пересоздать их вручную, второй раз - не получилось ничего. Учитывая пункт 1, решать пункт 2 я не стал.</p>
</li>
</ul>


<h2>Хорошая попытка Mikrotik - но нет 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D1%85%D0%BE%D1%80%D0%BE%D1%88%D0%B0%D1%8F-%D0%BF%D0%BE%D0%BF%D1%8B%D1%82%D0%BA%D0%B0-mikrotik---%D0%BD%D0%BE-%D0%BD%D0%B5%D1%82" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Технология докер контейнеров на RouterOS для меня лично отправляется на полку. Может когда-нибудь попробую еще раз.
В <a href="https://opnsense.org/" target="_blank">OPNsense</a> к примеру, функционал <code>AdGuardHome</code> добавляется как плагин. <strong>Все</strong>, оно сразу начинает работать, никаких <code>cold reboot</code> или ресетов, никаких бриджей и виртуальных интерфейсов, реестров и точек монтирования, и <code>AdGuardHome</code> видит всех внутренних клиентов сети.</p>
]]></content:encoded></item><item><title>ВПН не работает</title><link>https://andrey.dvur.me/posts/2023-08-10-forbidden-vpn/</link><guid isPermaLink="true">https://andrey.dvur.me/posts/2023-08-10-forbidden-vpn/</guid><pubDate>Thu, 10 Aug 2023 00:00:00 +0000</pubDate><category>format-article</category><category>index</category><category>comment-all</category><enclosure url="https://andrey.dvur.me/posts/2023-08-10-forbidden-vpn/featured.jpg" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>Приведенная схема используется мною исключительно для доступа к ресурсам, <strong>владельцы которых ограничили</strong> доступ к информации с российских IP. А таких технических иностранных ресурсов оказалось достаточное количество. Что там блокирует Роскомнадзор мне все равно, я и до его (Роскомнадзора) блокировок их не посещал.
{: .prompt-tip }</p>
</blockquote>


<h2>С понедельника иностранные VPN сервисы блокируются в мобильных сетях России 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D1%81-%D0%BF%D0%BE%D0%BD%D0%B5%D0%B4%D0%B5%D0%BB%D1%8C%D0%BD%D0%B8%D0%BA%D0%B0-%D0%B8%D0%BD%D0%BE%D1%81%D1%82%D1%80%D0%B0%D0%BD%D0%BD%D1%8B%D0%B5-vpn-%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D1%8B-%D0%B1%D0%BB%D0%BE%D0%BA%D0%B8%D1%80%D1%83%D1%8E%D1%82%D1%81%D1%8F-%D0%B2-%D0%BC%D0%BE%D0%B1%D0%B8%D0%BB%D1%8C%D0%BD%D1%8B%D1%85-%D1%81%D0%B5%D1%82%D1%8F%D1%85-%D1%80%D0%BE%D1%81%D1%81%D0%B8%D0%B8" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<ul>
<li>Блокируется VPN доступ в мобильной сети до <strong>иностранных</strong> серверов</li>
<li>Работает VPN доступ в мобильной сети к <strong>российским</strong> серверам</li>
<li>VPN работает в проводных сетях</li>
</ul>
<p>У меня есть <a href="https://andrey.dvur.me/posts/wireguard/" target="_blank">статья</a> по настройке wireguard сервера.</p>
<p><div align="center">
  <pre>
   flowchart LR
      a1[Клиент] --->|VPN|b3[IP в зоне EU/US]
          subgraph node2[Сервер]
            b3
          end
	  b3 --->C((Весь остальной мир IP))
</pre>
</div>

Теперь эта схема <strong>в мобильной сети</strong> не работает.</p>


<h2>Что же делать? 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D1%87%D1%82%D0%BE-%D0%B6%D0%B5-%D0%B4%D0%B5%D0%BB%D0%B0%D1%82%D1%8C" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Что может быть лучше одного VPN сервера? Правильно - два VPN сервера.
Первый VPN сервер нужен с российским IP адресом.</p>
<p>Но просто ввести в схему еще один сервер скучно, поэтому получим максимум из этой ситуации - добавим маршрутизацию по geoip.</p>
<div align="center">
  <pre>
   flowchart LR
      a1[Клиент] --->|WG|b1[IP в зоне RU]
	  b1 --->B((Российские IP))
          subgraph node2[Сервер 2]
            b3
          end
		       subgraph node1[Сервер 1]
            b1
          end
    b1 --->|WG| b3[IP в зоне EU/US]
	  b3 --->C((Весь остальной мир IP))
</pre>
</div>

<ul>
<li>Подключение мобильного клиента к серверу 1 работает, т.к. он находится на территории России</li>
<li>Подключение сервера 1 к серверу 2 работает - используется проводная сеть</li>
<li>За счет использования гео-маршрутизации на сервере 1: российские сервисы открываются через российский сервер, все остальные ресурсы отправляются через иностранный сервер</li>
</ul>
<p>
    <figure>
      <img loading="lazy" src="/pics/site/vpn-gate.jpeg" alt="" />
      
    </figure>
</p>


<h2>Теперь нет необходимости включать/отключать VPN 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D1%82%D0%B5%D0%BF%D0%B5%D1%80%D1%8C-%D0%BD%D0%B5%D1%82-%D0%BD%D0%B5%D0%BE%D0%B1%D1%85%D0%BE%D0%B4%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D0%B8-%D0%B2%D0%BA%D0%BB%D1%8E%D1%87%D0%B0%D1%82%D1%8C%D0%BE%D1%82%D0%BA%D0%BB%D1%8E%D1%87%D0%B0%D1%82%D1%8C-vpn" aria-label="Якорь">#</a>
    </span>        
    
</h2>


<h2>Реализация 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D1%80%D0%B5%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Для клиента и сервера 2 ничего не меняется - можно использовать мою <a href="https://andrey.dvur.me/posts/wireguard/" target="_blank">статью</a> по настройке wireguard сервера.</p>
<p>А вот для сервера 1 потребуется специализированное ПО - я рекомендую <a href="https://opnsense.org/" target="_blank">OPNsense</a>.
Как установить OPNsense на сервер, базовая настройка, настройка wireguard и geoip маршрутизации - тема очень объемная и выходит за рамки данной обзорной статьи.</p>
]]></content:encoded></item><item><title>Запрещаем использование иностранной почты при регистрации на российских сайтах</title><link>https://andrey.dvur.me/posts/2023-08-01-forbidden-email/</link><guid isPermaLink="true">https://andrey.dvur.me/posts/2023-08-01-forbidden-email/</guid><pubDate>Tue, 01 Aug 2023 00:00:00 +0000</pubDate><category>format-article</category><category>index</category><category>comment-all</category><enclosure url="https://andrey.dvur.me/posts/2023-08-01-forbidden-email/featured.jpg" type="image/jpeg"/><content:encoded><![CDATA[<p>Как же правильно определить «иностранность» почты?
Если ваша почта оканчивается на @gmail.com все понятно. А если вы используете (как я) почту со своим/корпоративным доменом?</p>
<p>Как вариант - можно определить путем анализа MX записи домена:</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>dig +short example.ru MX
</span></span></code></pre></div><p>А если используется частный почтовый сервис? Остается анализировать адрес IP на принадлежность его к российскому сегменту:</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>geoiplookup mail.example.ru
</span></span></code></pre></div><p>Это будет на момент регистрации, но почту можно мигрировать на вражеский сервис/ip уже после регистрации. Как доказать что проверка была выполнена и избежать штрафов?</p>
<p>Составлять на каждого пользователя протокол о должной осмотрительности..</p>
]]></content:encoded></item><item><title>Мониторинг Proxmox в Grafana</title><link>https://andrey.dvur.me/posts/2023-04-24-pve-exporter/</link><guid isPermaLink="true">https://andrey.dvur.me/posts/2023-04-24-pve-exporter/</guid><pubDate>Mon, 24 Apr 2023 00:00:00 +0000</pubDate><category>format-article</category><category>index</category><category>comment-all</category><enclosure url="https://andrey.dvur.me/posts/2023-04-24-pve-exporter/featured.jpg" type="image/jpeg"/><content:encoded><![CDATA[

<h2>В этой статье 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B2-%D1%8D%D1%82%D0%BE%D0%B9-%D1%81%D1%82%D0%B0%D1%82%D1%8C%D0%B5" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Расскажу как настроить мониторинг хостов/кластера Proxmox и отображать в Grafana состояние запущенных контейнеров и виртуальных машин.</p>
<p>Как запустить мониторинг Grafana/Prometheus за 5 минут можно прочитать в статье <a href="https://andrey.dvur.me/posts/grafana/" target="_blank">У тебя есть метрики? Дай!</a></p>


<h2>Настройки Proxmox VE 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B8-proxmox-ve" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>На хосте Proxmox создадим группу и пользователя для мониторинга и назначим соответствующую роль</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>pveum groupadd monitoring -comment <span>&#39;Monitoring group&#39;</span>
</span></span><span><span>pveum aclmod / -group monitoring -role PVEAuditor
</span></span><span><span>pveum useradd pve_exporter@pve
</span></span><span><span>pveum usermod pve_exporter@pve -group monitoring
</span></span><span><span>pveum passwd pve_exporter@pve
</span></span></code></pre></div>

<h2>Устанавливаем PVE-exporter 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D1%83%D1%81%D1%82%D0%B0%D0%BD%D0%B0%D0%B2%D0%BB%D0%B8%D0%B2%D0%B0%D0%B5%D0%BC-pve-exporter" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<div><pre tabindex="0"><code data-lang="bash"><span><span>apt update
</span></span><span><span>apt install python3-pip
</span></span><span><span>pip3 install prometheus-pve-exporter
</span></span></code></pre></div><p>Создаем файл настроек для PVE-exporter</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>mkdir -p /usr/share/pve_exporter/
</span></span><span><span>cat &gt; /usr/share/pve_exporter/pve_exporter.yml <span>&lt;&lt; EOF
</span></span></span><span><span><span>default:
</span></span></span><span><span><span>    user: pve_exporter@pve
</span></span></span><span><span><span>    password: пароль_пользователя_pve_exporter
</span></span></span><span><span><span>    verify_ssl: false
</span></span></span><span><span><span>EOF</span>
</span></span></code></pre></div><p>Запускаем</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>/usr/local/bin/pve_exporter /usr/share/pve_exporter/pve_exporter.yml
</span></span></code></pre></div>

<h2>Настройки Prometheus 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B8-prometheus" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>В файл конфигурации Prometheus добавим</p>
<div><pre tabindex="0"><code data-lang="yaml"><span><span><span>  </span>- <span>job_name</span><span>:</span><span> </span><span>&#39;pve&#39;</span><span>
</span></span></span><span><span><span>    </span><span>static_configs</span><span>:</span><span>
</span></span></span><span><span><span>      </span>- <span>targets</span><span>:</span><span>
</span></span></span><span><span><span>        </span>- <span>192.168.1.1</span><span>:</span><span>9221</span><span>  </span><span># Хост 1 Proxmox VE с PVE exporter</span><span>
</span></span></span><span><span><span>        </span>- <span>192.168.1.2</span><span>:</span><span>9221</span><span>  </span><span># Хост 2 Proxmox VE с PVE exporter</span><span>
</span></span></span><span><span><span>    </span><span>metrics_path</span><span>:</span><span> </span><span>/pve</span><span>
</span></span></span><span><span><span>    </span><span>params</span><span>:</span><span>
</span></span></span><span><span><span>      </span><span>module</span><span>:</span><span> </span><span>[</span><span>default]</span><span>
</span></span></span></code></pre></div><p>Здесь можно указать сразу несколько хостов Proxmox для мониторинга.
После изменения настроек перезапускаем сервис/контейнер Prometheus.</p>


<h2>Настройки Grafana 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B8-grafana" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Импортируем дашборд <a href="https://grafana.com/grafana/dashboards/10347-proxmox-via-prometheus/" target="_blank">№10347</a> в графану</p>
<p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-04-24-pve-exporter/grafana-8_hu_5de38dc3405cb0b7.jpg 330w,
        /posts/2023-04-24-pve-exporter/grafana-8_hu_135669f3d31a4d5c.jpg 660w,
        /posts/2023-04-24-pve-exporter/grafana-8_hu_6334f50d45865401.jpg 1024w,
        /posts/2023-04-24-pve-exporter/grafana-8_hu_325f7db1c77488d7.jpg 2x"
        data-zoom-src="/posts/2023-04-24-pve-exporter/grafana-8_hu_325f7db1c77488d7.jpg"
        src="/posts/2023-04-24-pve-exporter/grafana-8_hu_135669f3d31a4d5c.jpg"
        alt=""
      />
      
    </figure>
</p>


<h2>Наслаждаемся 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%BD%D0%B0%D1%81%D0%BB%D0%B0%D0%B6%D0%B4%D0%B0%D0%B5%D0%BC%D1%81%D1%8F" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-04-24-pve-exporter/grafana-10347_hu_bee356e9cfe65e5a.jpg 330w,
        /posts/2023-04-24-pve-exporter/grafana-10347_hu_aa1ac47f821b29d6.jpg 660w,
        /posts/2023-04-24-pve-exporter/grafana-10347_hu_9b788866a43ba690.jpg 1024w,
        /posts/2023-04-24-pve-exporter/grafana-10347_hu_210b6d92fdb9491d.jpg 2x"
        data-zoom-src="/posts/2023-04-24-pve-exporter/grafana-10347_hu_210b6d92fdb9491d.jpg"
        src="/posts/2023-04-24-pve-exporter/grafana-10347_hu_aa1ac47f821b29d6.jpg"
        alt=""
      />
      
    </figure>
</p>
<p>Для отслеживания кластера - достаточно установить PVE-exporter на любой узел (для надежности можно и на все).</p>
]]></content:encoded></item><item><title>Reverse proxy - обратный прокси, как и зачем</title><link>https://andrey.dvur.me/posts/2023-04-20-reverse-proxy/</link><guid isPermaLink="true">https://andrey.dvur.me/posts/2023-04-20-reverse-proxy/</guid><pubDate>Thu, 20 Apr 2023 00:00:00 +0000</pubDate><category>format-article</category><category>index</category><category>comment-all</category><enclosure url="https://andrey.dvur.me/posts/2023-04-20-reverse-proxy/featured.jpg" type="image/jpeg"/><content:encoded><![CDATA[<p>Если в домашней сети живет один Nextcloud или какой-нибудь медиа-сервер вроде Plex  - обратный прокси вам не сдался.
Когда домашних сервисов переваливает за десяток - помнить и вбивать их адреса и <strong>порты</strong> начинает уже утомлять.</p>


<h2>Общая схема 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%BE%D0%B1%D1%89%D0%B0%D1%8F-%D1%81%D1%85%D0%B5%D0%BC%D0%B0" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<div align="center">
  <pre>
   flowchart LR
      a1[nextcloud.doma.net] ---> B((Обратный прокси сервер))
      a2[speedtest.doma.net] ---> B((Обратный прокси сервер))
      a3[plex.doma.net] ---> B((Обратный прокси сервер))
      B ---> b1[192.168.88.10:8080]
      B ---> b2[192.168.88.10:9000]
      B ---> b3[192.168.88.20:3000]
          subgraph node1[Сервер 1]
            b1
            b2
          end
          subgraph node2[Сервер 2]
            b3
          end
</pre>
</div>

<p>Домустим у нас есть три сервиса:</p>
<ul>
<li>наш внутренний или внешний домен <code>doma.net</code></li>
<li>Nextcloud - работает на сервере <code>192.168.88.20</code>, порт <code>3000</code></li>
<li>Сервис для измерения скорости speedtest - работает на сервере <code>192.168.88.10</code>, порт <code>8080</code></li>
<li>Медиа сервер Plex - - работает на сервере <code>192.168.88.10</code>, порт <code>9000</code></li>
</ul>


<h3>Зачем нам обратный прокси, нельзя ли обойтись DNS? 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B7%D0%B0%D1%87%D0%B5%D0%BC-%D0%BD%D0%B0%D0%BC-%D0%BE%D0%B1%D1%80%D0%B0%D1%82%D0%BD%D1%8B%D0%B9-%D0%BF%D1%80%D0%BE%D0%BA%D1%81%D0%B8-%D0%BD%D0%B5%D0%BB%D1%8C%D0%B7%D1%8F-%D0%BB%D0%B8-%D0%BE%D0%B1%D0%BE%D0%B9%D1%82%D0%B8%D1%81%D1%8C-dns" aria-label="Якорь">#</a>
    </span>        
    
</h3>
<p>Частично - можно. DNS позволит сделать связку <code>имя ---&gt; ip-адрес</code>. В браузере все равно придется вбивать еще и порты:</p>
<ul>
<li><code>nextcloud.doma.net:3000</code></li>
<li><code>speedtest.doma.net:8080</code></li>
<li><code>plex.doma.net:9000</code></li>
</ul>
<p>При этом, <code>speedtest</code> и <code>plex</code> запущены на одном сервере <code>192.168.88.10</code>. Т.е фактически получится, что:</p>
<ul>
<li><code>plex</code> будет доступен еще и по адресу <code>speedtest.doma.net:9000</code></li>
<li>а в сервис <code>speedtest</code> можно зайти по адресу <code>plex.doma.net:8080</code></li>
</ul>
<p>Такая вот чехарда с DNS.</p>
<p>Используя обратный прокси - мы эти три записи <code>nextcloud.doma.net</code>, <code>speedtest.doma.net</code> и  <code>plex.doma.net</code> с помощью DNS направляем на адрес прокси сервера. А уже он, в зависимости от того, какой адрес ему поступил перенаправит запрос к нужному ip-адресу с указанным портом.</p>


<h2>Практика 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%BF%D1%80%D0%B0%D0%BA%D1%82%D0%B8%D0%BA%D0%B0" aria-label="Якорь">#</a>
    </span>        
    
</h2>


<h3>Запускаем прокси 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B7%D0%B0%D0%BF%D1%83%D1%81%D0%BA%D0%B0%D0%B5%D0%BC-%D0%BF%D1%80%D0%BE%D0%BA%D1%81%D0%B8" aria-label="Якорь">#</a>
    </span>        
    
</h3>
<p>Обратных прокси-серверов существует несколько - traefik, caddy, nging и др. Cамый интуитивно понятный, на мой взгляд -  <a href="https://nginxproxymanager.com/" target="_blank">Nginx Proxy Manager</a>. С ним и будем работать.</p>
<p>Делать будем просто и быстро - в контейнерах докера. <a href="https://docs.docker.com/engine/install/ubuntu/" target="_blank">Для этого нам понадобится сам docker и docker compose.</a></p>
<p><strong>Nginx Proxy Manager</strong> можно запускать на отдельном узле, а можно на одном узле вместе с остальными контейнерами.
В нашем примере - допустим мы запускаем наш прокси на сервере 2, на одной машине вместе с <code>nextcloud</code>.</p>
<p>Создадим папку</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>mkdir -p nginxproxy/<span>{</span>data,letsencrypt<span>}</span>
</span></span><span><span><span>cd</span> nginxproxy
</span></span></code></pre></div><p>Создадим файл запуска контейнера</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>nano docker-compose.yml
</span></span></code></pre></div><p>И вставим  в него следующее содержимое</p>
<div><pre tabindex="0"><code data-lang="yaml"><span><span><span>version</span><span>:</span><span> </span><span>&#39;3.8&#39;</span><span>
</span></span></span><span><span><span></span><span>services</span><span>:</span><span>
</span></span></span><span><span><span>  </span><span>app</span><span>:</span><span>
</span></span></span><span><span><span>    </span><span>image</span><span>:</span><span> </span><span>&#39;jc21/nginx-proxy-manager:latest&#39;</span><span>
</span></span></span><span><span><span>    </span><span>restart</span><span>:</span><span> </span><span>unless-stopped</span><span>
</span></span></span><span><span><span>    </span><span>ports</span><span>:</span><span>
</span></span></span><span><span><span>      </span>- <span>&#39;80:80&#39;</span><span>
</span></span></span><span><span><span>      </span>- <span>&#39;81:81&#39;</span><span>
</span></span></span><span><span><span>      </span>- <span>&#39;443:443&#39;</span><span>
</span></span></span><span><span><span>    </span><span>volumes</span><span>:</span><span>
</span></span></span><span><span><span>      </span>- <span>./data:/data</span><span>
</span></span></span><span><span><span>      </span>- <span>./letsencrypt:/etc/letsencrypt</span><span>
</span></span></span></code></pre></div><ul>
<li><code>jc21/nginx-proxy-manager:latest</code> - образ <strong>Nginx Proxy Manager</strong></li>
<li><code>restart: unless-stopped</code> - контейнер будет перезапускаться при сбоях, если только не остановлен вручную</li>
<li><code>ports</code> - открытые порты: 80 и 443 для web-трафика, порт 81 для администрирования самого <strong>Nginx Proxy Manager</strong></li>
<li><code>volumes</code> - папки, смонтированные внутрь контейнера. <code>data</code> для разных внутренних данных и логов. А в папке <code>letsencrypt</code> будут храниться сертификаты, выпущенные одноименным сервисом.</li>
</ul>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo docker compose up -d
</span></span></code></pre></div><p>Ждем, когда скачается образ и запустится контейнер.</p>
<p>В этом примере <strong>Nginx Proxy Manager</strong> запускаем на сервере 2, поэтому вбиваем в браузер адрес этого сервера и добавляем порт администрирования
<code>http://192.168.88.20:81</code></p>
<p>
    <figure>
      <img loading="lazy" src="/pics/posts/2023-04-20/npm-login.jpg" alt="" />
      
    </figure>
</p>
<p>Первоначальные логин и пароль</p>
<pre tabindex="0"><code>Email:    admin@example.com
Password: changeme
</code></pre>

<h3>Настройки 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B8" aria-label="Якорь">#</a>
    </span>        
    
</h3>
<p>Первым делом меняем логин/пароль администратора.</p>
<p>Вторым действием - сразу добавим шифрование. В  2023 большинство современных браузеров откажется открывать сайт без хоть какого-нибудь ssl сертификата.</p>
<p>Как <a href="https://andrey.dvur.me/posts/selfsigned-cert/" target="_blank">легко создать самоподписные сертификаты</a> у меня есть небольшая статья.</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>openssl req -x509 -newkey rsa:4096 -sha256 -days <span>3650</span> -nodes <span>\
</span></span></span><span><span><span></span>  -keyout domanet.key -out domanet.crt -subj <span>&#34;/CN=*.doma.net&#34;</span> <span>\
</span></span></span><span><span><span></span>  -addext <span>&#34;subjectAltName=DNS:*.doma.net&#34;</span> 
</span></span></code></pre></div><p>Это командой создается ключ и сертификат для <strong>всех</strong> поддоменов <code>*.doma.net</code></p>
<p>Перейдем в <strong>Nginx Proxy Manager</strong> на вкладку <code>SSL Certificates ---&gt; Add SSL Certificate ---&gt; Custom</code> и добавим только что сгенерированный сертификат и ключ к нему.</p>
<p>
    <figure>
      <img loading="lazy" src="/pics/posts/2023-04-20/doma-cert.jpg" alt="" />
      
    </figure>
</p>
<p>Теперь к конфигурированию наших сервисов.
Вкладка <code>Hosts ---&gt; Proxy Hosts ---&gt; Add Proxy Host</code>
Вводим данные для <code>Nextcloud</code>

    <figure>
      <img loading="lazy" src="/pics/posts/2023-04-20/nextcloud-npm.jpg" alt="" />
      
    </figure>
</p>
<p>На вкладке <code>SSL</code> - добавляем сертификат

    <figure>
      <img loading="lazy" src="/pics/posts/2023-04-20/nextcloud-npm-ssl.jpg" alt="" />
      
    </figure>

Жмем <code>Save</code>.</p>


<h2>Результат 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D1%80%D0%B5%D0%B7%D1%83%D0%BB%D1%8C%D1%82%D0%B0%D1%82" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<div align="center">
  <pre>
   flowchart LR
      a1[nextcloud.doma.net] --->|DNS|B((Обратный прокси сервер))
          subgraph node2[Сервер 2]
            b3
          end
      B ---> b3[192.168.88.20:3000]
</pre>
</div>

<p>Теперь при вводе в строке браузера <code>nextcloud.doma.net</code> DNS перенаправит наш запрос на прокси сервер, а он уже перенаправит запрос на сервер <code>192.168.88.20:3000</code></p>


<h2>А еще 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B0-%D0%B5%D1%89%D0%B5" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<ul>
<li>Можно генерировать сертификаты Let&rsquo;s Encrypt, которые будут самообновляться</li>
<li>Добавить дополнительную аутентификацию для сервисов</li>
<li>Разрешать или запрещать доступ с определенных ip-адресов или сетей.</li>
</ul>


<h2>Комментарии и вопросы в <a href="https://t.me/andrey_dvur" target="_blank">Телеграм</a> или <a href="https://dzen.ru/andrey_dvur" target="_blank">Дзен</a> 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%BA%D0%BE%D0%BC%D0%BC%D0%B5%D0%BD%D1%82%D0%B0%D1%80%D0%B8%D0%B8-%D0%B8-%D0%B2%D0%BE%D0%BF%D1%80%D0%BE%D1%81%D1%8B-%D0%B2-%D1%82%D0%B5%D0%BB%D0%B5%D0%B3%D1%80%D0%B0%D0%BC-%D0%B8%D0%BB%D0%B8-%D0%B4%D0%B7%D0%B5%D0%BD" aria-label="Якорь">#</a>
    </span>        
    
</h2>
]]></content:encoded></item><item><title>Шифрование внешнего диска в Linux</title><link>https://andrey.dvur.me/posts/2023-04-02-cryptsetup/</link><guid isPermaLink="true">https://andrey.dvur.me/posts/2023-04-02-cryptsetup/</guid><pubDate>Sun, 02 Apr 2023 00:00:00 +0000</pubDate><category>format-article</category><category>index</category><category>comment-all</category><content:encoded><![CDATA[

<h2>Дано 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B4%D0%B0%D0%BD%D0%BE" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Я забыл пароль от внешнего зашифрованного диска, поэтому решил заново его инициализировать и написать памятку для себя будущего.</p>
<ul>
<li>система - Linux</li>
<li>внешний диск - 1TB</li>
</ul>


<h2>Теория 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D1%82%D0%B5%D0%BE%D1%80%D0%B8%D1%8F" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Обычный внешний диск мы сразу монтируем в какую-нибудь папку.</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo mount &lt;устройство&gt; &lt;папка&gt;
</span></span></code></pre></div><p>В случае с шифрованием появляется промежуточный шаг - открытие контейнера. Сначала мы открываем внешний диск как контейнер, а уже потом этот контейнер монтируем как папку.</p>


<h2>Практика 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%BF%D1%80%D0%B0%D0%BA%D1%82%D0%B8%D0%BA%D0%B0" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Устанавливаем утилиту <a href="https://gitlab.com/cryptsetup/cryptsetup" target="_blank">cryptsetup</a>, она есть практически в любом дистрибутиве.</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo apt install cryptsetup
</span></span></code></pre></div><p>Смотрим под каким устройством появляется наш внешний диск</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>ls -lah /dev/sd*
</span></span></code></pre></div><p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-04-02-cryptsetup/dev-sdb_hu_41f8301fb38fd0b8.jpg 330w,
        /posts/2023-04-02-cryptsetup/dev-sdb_hu_f03fc40233d34ee0.jpg 660w,
        /posts/2023-04-02-cryptsetup/dev-sdb_hu_aba642fac62c8616.jpg 1024w,
        /posts/2023-04-02-cryptsetup/dev-sdb_hu_904667e9cf0814d3.jpg 2x"
        data-zoom-src="/posts/2023-04-02-cryptsetup/dev-sdb_hu_904667e9cf0814d3.jpg"
        src="/posts/2023-04-02-cryptsetup/dev-sdb_hu_f03fc40233d34ee0.jpg"
        alt=""
      />
      
    </figure>

В моем случае <code>/dev/sda</code> - системный диск.
А вот <code>/dev/sdb</code> - внешний, с ним и будем работать.</p>

  
  
  
  



<div
  
   
    style="background-color: #e63946"
  >

  <span
    
     
      style="color: #1d3557"
    >

    

  <span>
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path fill="currentColor"  d="M159.3 5.4c7.8-7.3 19.9-7.2 27.7 .1c27.6 25.9 53.5 53.8 77.7 84c11-14.4 23.5-30.1 37-42.9c7.9-7.4 20.1-7.4 28 .1c34.6 33 63.9 76.6 84.5 118c20.3 40.8 33.8 82.5 33.8 111.9C448 404.2 348.2 512 224 512C98.4 512 0 404.1 0 276.5c0-38.4 17.8-85.3 45.4-131.7C73.3 97.7 112.7 48.6 159.3 5.4zM225.7 416c25.3 0 47.7-7 68.8-21c42.1-29.4 53.4-88.2 28.1-134.4c-2.8-5.6-5.6-11.2-9.8-16.8l-50.6 58.8s-81.4-103.6-87.1-110.6C133.1 243.8 112 273.2 112 306.8C112 375.4 162.6 416 225.7 416z"/></svg>
  </span>


  </span>

  <span
    
      style="color: #f1faee"
    >Шифрование уничтожит все данные на диске, важно сохранить всю информацию перед своими действиями и не перепутать устройство.</span>
</div>

<p>Форматируем диск - указываем <code>YES</code>, что мы понимаем, что потеряем все данные на диске и два раза вводим парольную фразу.</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo cryptsetup luksFormat /dev/sdb
</span></span></code></pre></div><p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-04-02-cryptsetup/luksformat_hu_d7b726eb33b543f7.png 330w,
        /posts/2023-04-02-cryptsetup/luksformat_hu_3e03e148ce3976b2.png 660w,
        /posts/2023-04-02-cryptsetup/luksformat_hu_262d7c209ea8b637.png 1024w,
        /posts/2023-04-02-cryptsetup/luksformat_hu_502900144e49d9a9.png 2x"
        data-zoom-src="/posts/2023-04-02-cryptsetup/luksformat_hu_502900144e49d9a9.png"
        src="/posts/2023-04-02-cryptsetup/luksformat_hu_3e03e148ce3976b2.png"
        alt=""
      />
      
    </figure>
</p>
<p>Теперь откроем созданный контейнер (<code>enc1TB</code> - я назвал свое устройство)</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo cryptsetup open /dev/sdb enc1TB
</span></span></code></pre></div><p>После этого наше уже расшифрованное устройство появится в директории <code>/dev/mapper/</code></p>
<p>Создадим файловую систему</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo mkfs.ext4 /dev/mapper/enc1TB
</span></span></code></pre></div><p>и смонтируем в директорию <code>/mnt/1TB</code></p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo mount /dev/mapper/enc1TB /mnt/1TB
</span></span></code></pre></div><p>Проверим</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>df -h
</span></span></code></pre></div><p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-04-02-cryptsetup/df-h_hu_9bc06a87f3d408d5.jpg 330w,
        /posts/2023-04-02-cryptsetup/df-h_hu_8a4d1d24df7608b0.jpg 660w,
        /posts/2023-04-02-cryptsetup/df-h_hu_10efd87b2591b547.jpg 1024w,
        /posts/2023-04-02-cryptsetup/df-h_hu_3d6d4f449ba3047a.jpg 2x"
        data-zoom-src="/posts/2023-04-02-cryptsetup/df-h_hu_3d6d4f449ba3047a.jpg"
        src="/posts/2023-04-02-cryptsetup/df-h_hu_8a4d1d24df7608b0.jpg"
        alt=""
      />
      
    </figure>
</p>


<h2>Можем работать с зашифрованным диском 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%BC%D0%BE%D0%B6%D0%B5%D0%BC-%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%B0%D1%82%D1%8C-%D1%81-%D0%B7%D0%B0%D1%88%D0%B8%D1%84%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D1%8B%D0%BC-%D0%B4%D0%B8%D1%81%D0%BA%D0%BE%D0%BC" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Открыть диск</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo cryptsetup open /dev/sdb enc1TB
</span></span><span><span>sudo mount /dev/mapper/enc1TB /mnt/1TB
</span></span></code></pre></div><p>Закрыть диск</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo umount /mnt/1TB
</span></span><span><span>sudo cryptsetup close enc1TB
</span></span></code></pre></div>

<h1>Памятка себе будущему 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%BF%D0%B0%D0%BC%D1%8F%D1%82%D0%BA%D0%B0-%D1%81%D0%B5%D0%B1%D0%B5-%D0%B1%D1%83%D0%B4%D1%83%D1%89%D0%B5%D0%BC%D1%83" aria-label="Якорь">#</a>
    </span>        
    
</h1>
<p>Андрей, не путай пароль <code>sudo</code> и <code>passphrase</code> диска. Я не забыл пароль от внешнего диска, я по невнимательности вбивал его, когда требовался пароль суперпользователя.</p>
]]></content:encoded></item><item><title>У тебя есть метрики? Дай! Или мониторинг с помощью Prometheus и Grafana за 5 минут</title><link>https://andrey.dvur.me/posts/2023-03-23-grafana/</link><guid isPermaLink="true">https://andrey.dvur.me/posts/2023-03-23-grafana/</guid><pubDate>Mon, 27 Mar 2023 00:00:00 +0000</pubDate><category>format-article</category><category>index</category><category>comment-all</category><enclosure url="https://andrey.dvur.me/grafana-dashboard-panels-1200x900-1.jpeg" type="image/jpeg"/><content:encoded><![CDATA[<p>Теорию о пользе и важности мониторинга я опущу. Расскажу на практике: <a href="https://andrey.dvur.me/posts/traffic-leak/" target="_blank">утечка трафика, которую я случайно обнаружил в январе</a> - с монторингом не случилась бы в принципе. Мониторинг может рассылать уведомления о превышении каких-то параметров, а расследование причин в моем конкретном случае заняло бы пару минут, а не часов.</p>


<h1>TL:DR 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#tldr" aria-label="Якорь">#</a>
    </span>        
    
</h1>
<ol>
<li><a href="https://docs.docker.com/engine/install/ubuntu/" target="_blank">Устанавливаем docker и docker compose.</a></li>
<li>Скачиваем из <a href="https://github.com/andrey-dvur/Scripts/tree/main/Prometheus-Grafana" target="_blank">репозитория Github</a> конфигурационные файлы и изменяем их под свою сеть</li>
<li>Выполняем комманду <code>sudo docker compose up -d</code></li>
<li>Устанавливаем на узлы <a href="https://prometheus.io/download/#node_exporter" target="_blank">Node exporter</a></li>
</ol>


<h1>Подробнее 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%BF%D0%BE%D0%B4%D1%80%D0%BE%D0%B1%D0%BD%D0%B5%D0%B5" aria-label="Якорь">#</a>
    </span>        
    
</h1>


<h2>Общая схема 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%BE%D0%B1%D1%89%D0%B0%D1%8F-%D1%81%D1%85%D0%B5%D0%BC%D0%B0" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-03-23-grafana/nodes-db-grafana-r_hu_2677479c978610a7.jpg 330w,
        /posts/2023-03-23-grafana/nodes-db-grafana-r_hu_bf8253d1d1b21500.jpg 660w,
        /posts/2023-03-23-grafana/nodes-db-grafana-r_hu_11d5eea942a0ce7d.jpg 1024w,
        /posts/2023-03-23-grafana/nodes-db-grafana-r_hu_b893834b2cbbb8a1.jpg 2x"
        data-zoom-src="/posts/2023-03-23-grafana/nodes-db-grafana-r_hu_b893834b2cbbb8a1.jpg"
        src="/posts/2023-03-23-grafana/nodes-db-grafana-r_hu_bf8253d1d1b21500.jpg"
        alt=""
      />
      
    </figure>
</p>
<ul>
<li>На устройстве, которое мы хотим мониторить, запускается сервис по сбору различной телеметрии (использование ЦП, памяти, сети и пр.)</li>
<li>Эта информация предается на центральный узел, где собирается и хранится в базе данных</li>
<li>Grafana берет данные из этой базы и рисует нам красивые графики</li>
</ul>
<p>Вариантов реализации много, я использовал два, а подробно разберу один:</p>
<ol>
<li>
<p><code>Telegraf ---&gt; InfluxDB &lt;--- Grafana</code> В данном варианте, на клиенты устанавливается программа <a href="https://docs.influxdata.com/telegraf/v1.25/install/" target="_blank"><strong>Telegraf</strong></a> -  она собирает данные и отправляет на центральный узел. На центральном узле используется база данных <a href="https://www.influxdata.com/" target="_blank"><strong>InfluxDB</strong></a>, куда прилетают метрики от Telegraf&rsquo;a, а также откуда берет данные графана для построения графиков.
Старое и проверенное решение, но мониторить получиться компьютеры, на которые можно установить Telegraf.</p>
</li>
<li>
<p><code>Node exporter &lt;--- Prometheus &lt;--- Grafana</code> Более современное решение, его и разберу подробнее.
В качестве сборщика данных используется сервис <a href="https://prometheus.io/" target="_blank"><strong>Prometheus</strong></a>. На узлах, которые мы собираемся мониторить - устанавливается программа <a href="https://prometheus.io/download/#node_exporter" target="_blank"><strong>Node exporter</strong></a>. Отличие от первого варианта <strong>Node exporter</strong> собирает , но не отправляет метрики на центральный узел.
<strong>Prometheus</strong> сам опрашивает узлы, согласно настройке в конфиг-файле. Графана в первом и во втором случае работает одинаково - она берет данные из центральной базы.</p>
</li>
</ol>


<h2>Докер 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B4%D0%BE%D0%BA%D0%B5%D1%80" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Делать будем все в контейнерах докера - самый простой и быстрый способ. <a href="https://docs.docker.com/engine/install/ubuntu/" target="_blank">Для этого нам понадобится сам docker и docker compose.</a></p>
<p>Скачиваем конфигурационные файлы</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>mkdir -p prometheus/<span>{</span>config,snmp_exporter<span>}</span>
</span></span><span><span><span>cd</span> prometheus
</span></span><span><span>wget https://raw.githubusercontent.com/andrey-dvur/Scripts/main/Prometheus-Grafana/docker-compose.yml
</span></span><span><span><span>cd</span> config
</span></span><span><span>wget https://raw.githubusercontent.com/andrey-dvur/Scripts/main/Prometheus-Grafana/config/prometheus.yml
</span></span><span><span><span>cd</span> ../snmp_exporter
</span></span><span><span>wget https://raw.githubusercontent.com/prometheus/snmp_exporter/snmp.yml
</span></span></code></pre></div><p>Что же мы скачали:</p>
<ul>
<li>файл <code>docker-compose.yml</code> описывает какие докер контейнеры и с какими параметрами мы будем запускать. На начальном этапе это два контейнера - Prometheus и Grafana.</li>
<li>в папку <code>config</code> скачался файл настроек <code>prometheus.yml</code></li>
<li>про файл <code>snmp.yml</code> в папке <code>snmp_exporter</code> расскажу чуть ниже</li>
</ul>
<p>Указываем в файле <code>prometheus.yml</code> узлы, которые будем мониторить</p>
<div><pre tabindex="0"><code data-lang="yml"><span><span><span>#########  HOMELAB ######################</span><span>
</span></span></span><span><span><span>  </span>- <span>job_name</span><span>:</span><span> </span><span>&#39;node.A&#39;</span><span> </span><span># Имя узла</span><span>
</span></span></span><span><span><span>    </span><span>static_configs</span><span>:</span><span>
</span></span></span><span><span><span>      </span>- <span>targets</span><span>:</span><span> </span><span>[</span><span>&#39;192.168.88.51:9100&#39;</span><span>]</span><span> </span><span># hostname/ip-адрес узла, порт 9100 - стандартный для node-exporter</span><span>
</span></span></span><span><span><span>  </span>- <span>job_name</span><span>:</span><span> </span><span>&#39;node.B&#39;</span><span>
</span></span></span><span><span><span>    </span><span>static_configs</span><span>:</span><span>
</span></span></span><span><span><span>      </span>- <span>targets</span><span>:</span><span> </span><span>[</span><span>&#39;192.168.88.52:9100&#39;</span><span>]</span><span>
</span></span></span><span><span><span>  </span>- <span>job_name</span><span>:</span><span> </span><span>&#39;node.C&#39;</span><span>
</span></span></span><span><span><span>    </span><span>static_configs</span><span>:</span><span>
</span></span></span><span><span><span>      </span>- <span>targets</span><span>:</span><span> </span><span>[</span><span>&#39;192.168.88.53:9100&#39;</span><span>]</span><span>
</span></span></span></code></pre></div><blockquote>
<p>Тут используется синтаксис <code>YAML</code>, поэтому, да, важно количество пробелов-отступов
{: .prompt-info }</p>
</blockquote>
<p>Переходим в папку <code>prometheus</code>, где находится наш файл <code>docker-compose.yml</code> и запускаем наши контейнеры</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo docker compose up -d
</span></span></code></pre></div><p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-03-23-grafana/grafana-prometheus_hu_d48796b1e6cd4dfe.jpg 330w,
        /posts/2023-03-23-grafana/grafana-prometheus_hu_6000444fb49510a5.jpg 660w,
        /posts/2023-03-23-grafana/grafana-prometheus_hu_183f75d250dd61ec.jpg 1024w,
        /posts/2023-03-23-grafana/grafana-prometheus_hu_5b64b38dc2e249a4.jpg 2x"
        data-zoom-src="/posts/2023-03-23-grafana/grafana-prometheus_hu_5b64b38dc2e249a4.jpg"
        src="/posts/2023-03-23-grafana/grafana-prometheus_hu_6000444fb49510a5.jpg"
        alt=""
      />
      
    </figure>
</p>
<p>Ждем запуска обоих контейнеров, после этого открываем браузер, вбиваем ip-адрес нашего сервера, на ктором мы только что запустили контейнеры и порт графаны: 3000.</p>


<h2>Базовые настройки 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B1%D0%B0%D0%B7%D0%BE%D0%B2%D1%8B%D0%B5-%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B8" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p><code>http://ip-адрес:3000</code></p>
<p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-03-23-grafana/grafana-1-r_hu_32a9a340e20d4765.jpg 330w,
        /posts/2023-03-23-grafana/grafana-1-r_hu_eee7701f2df1fedb.jpg 660w,
        /posts/2023-03-23-grafana/grafana-1-r_hu_7f400bf4a6b9b5a5.jpg 1024w,
        /posts/2023-03-23-grafana/grafana-1-r_hu_4b52edb85b2e255a.jpg 2x"
        data-zoom-src="/posts/2023-03-23-grafana/grafana-1-r_hu_4b52edb85b2e255a.jpg"
        src="/posts/2023-03-23-grafana/grafana-1-r_hu_eee7701f2df1fedb.jpg"
        alt=""
      />
      
    </figure>
</p>
<p>Начальные логин/пароль <code>admin/admin</code></p>
<p>Выбираем, откуда графана будет брать данные - наш источник

    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-03-23-grafana/grafana-2_hu_d15fbfb75c820128.jpg 330w,
        /posts/2023-03-23-grafana/grafana-2_hu_835d0a3b6eec5d22.jpg 660w,
        /posts/2023-03-23-grafana/grafana-2_hu_50286c52a4d2609f.jpg 1024w,
        /posts/2023-03-23-grafana/grafana-2_hu_f277404e290204e7.jpg 2x"
        data-zoom-src="/posts/2023-03-23-grafana/grafana-2_hu_f277404e290204e7.jpg"
        src="/posts/2023-03-23-grafana/grafana-2_hu_835d0a3b6eec5d22.jpg"
        alt=""
      />
      
    </figure>
</p>
<p>Выбираем <code>Prometheus</code>

    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-03-23-grafana/grafana-3_hu_727eed26bf376848.jpg 330w,
        /posts/2023-03-23-grafana/grafana-3_hu_8b28fc2f88c4a32e.jpg 660w,
        /posts/2023-03-23-grafana/grafana-3_hu_120b95699f94d961.jpg 1024w,
        /posts/2023-03-23-grafana/grafana-3_hu_a18182f2d6062093.jpg 2x"
        data-zoom-src="/posts/2023-03-23-grafana/grafana-3_hu_a18182f2d6062093.jpg"
        src="/posts/2023-03-23-grafana/grafana-3_hu_8b28fc2f88c4a32e.jpg"
        alt=""
      />
      
    </figure>
</p>
<p>Адреc нашего <code>Prometheus</code> указываем <code>http://prometheus:9090</code></p>
<blockquote>
<p>докер понимает написание адреса в таком виде
{: .prompt-info }</p>
</blockquote>
<p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-03-23-grafana/grafana-4_hu_5676a894c696638b.jpg 330w,
        /posts/2023-03-23-grafana/grafana-4_hu_44def562f457ac96.jpg 660w,
        /posts/2023-03-23-grafana/grafana-4_hu_e7ee584a4697fb7c.jpg 1024w,
        /posts/2023-03-23-grafana/grafana-4_hu_884138b3b9703f01.jpg 2x"
        data-zoom-src="/posts/2023-03-23-grafana/grafana-4_hu_884138b3b9703f01.jpg"
        src="/posts/2023-03-23-grafana/grafana-4_hu_44def562f457ac96.jpg"
        alt=""
      />
      
    </figure>
</p>
<p>Внизу страницы нажимем <code>Save &amp; test</code>

    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-03-23-grafana/grafana-5_hu_d2d6558c1791e4e2.jpg 330w,
        /posts/2023-03-23-grafana/grafana-5_hu_946980cea7b33eae.jpg 660w,
        /posts/2023-03-23-grafana/grafana-5_hu_e4c4b92fafdfaffa.jpg 1024w,
        /posts/2023-03-23-grafana/grafana-5_hu_94e718d85c90b133.jpg 2x"
        data-zoom-src="/posts/2023-03-23-grafana/grafana-5_hu_94e718d85c90b133.jpg"
        src="/posts/2023-03-23-grafana/grafana-5_hu_946980cea7b33eae.jpg"
        alt=""
      />
      
    </figure>
</p>
<p>Ожидаем увидеть:

    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-03-23-grafana/grafana-6_hu_eb312d837430f910.jpg 330w,
        /posts/2023-03-23-grafana/grafana-6_hu_3651a7568ddea39a.jpg 660w,
        /posts/2023-03-23-grafana/grafana-6_hu_761068bab66a5578.jpg 1024w,
        /posts/2023-03-23-grafana/grafana-6_hu_274aa3e5e79066d4.jpg 2x"
        data-zoom-src="/posts/2023-03-23-grafana/grafana-6_hu_274aa3e5e79066d4.jpg"
        src="/posts/2023-03-23-grafana/grafana-6_hu_3651a7568ddea39a.jpg"
        alt=""
      />
      
    </figure>
</p>


<h2>Дашборды 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B4%D0%B0%D1%88%D0%B1%D0%BE%D1%80%D0%B4%D1%8B" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Источник данных настроили, перейдем к самой красоте - графикам.</p>
<p>Вообще <code>Prometheus</code> и сам может рисовать графики без графаны. Плюс графаны в данном случае - огромный выбор уже <a href="https://grafana.com/grafana/dashboards/" target="_blank">готовых наборов графиков (дашбордов)</a>, которые достаточно импортировать в свою конфигурацию.</p>
<p>Выберем на <a href="https://grafana.com/grafana/dashboards/" target="_blank">сайте</a> самый популярный дашборд

    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-03-23-grafana/grafana-9_hu_3b7f5aa3fbd740d.jpg 330w,
        /posts/2023-03-23-grafana/grafana-9_hu_3cbdb16d8a52c661.jpg 660w,
        /posts/2023-03-23-grafana/grafana-9_hu_1fd4e0c809b4f9b9.jpg 1024w,
        /posts/2023-03-23-grafana/grafana-9_hu_73578b4762cb1ad2.jpg 2x"
        data-zoom-src="/posts/2023-03-23-grafana/grafana-9_hu_73578b4762cb1ad2.jpg"
        src="/posts/2023-03-23-grafana/grafana-9_hu_3cbdb16d8a52c661.jpg"
        alt=""
      />
      
    </figure>
</p>
<p>Скопируем его ID

    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-03-23-grafana/grafana-10_hu_eb9cba17c07de5ef.jpg 330w,
        /posts/2023-03-23-grafana/grafana-10_hu_cd12843fcc410243.jpg 660w,
        /posts/2023-03-23-grafana/grafana-10_hu_c12b04502f5669a.jpg 1024w,
        /posts/2023-03-23-grafana/grafana-10_hu_1e4998095c2426e8.jpg 2x"
        data-zoom-src="/posts/2023-03-23-grafana/grafana-10_hu_1e4998095c2426e8.jpg"
        src="/posts/2023-03-23-grafana/grafana-10_hu_cd12843fcc410243.jpg"
        alt=""
      />
      
    </figure>
</p>
<p>Откроем импорт дашборда в нашей графане

    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-03-23-grafana/grafana-8_hu_5de38dc3405cb0b7.jpg 330w,
        /posts/2023-03-23-grafana/grafana-8_hu_135669f3d31a4d5c.jpg 660w,
        /posts/2023-03-23-grafana/grafana-8_hu_6334f50d45865401.jpg 1024w,
        /posts/2023-03-23-grafana/grafana-8_hu_325f7db1c77488d7.jpg 2x"
        data-zoom-src="/posts/2023-03-23-grafana/grafana-8_hu_325f7db1c77488d7.jpg"
        src="/posts/2023-03-23-grafana/grafana-8_hu_135669f3d31a4d5c.jpg"
        alt=""
      />
      
    </figure>
</p>
<p>вставим скопированный ID, жмем <code>Load</code>

    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-03-23-grafana/grafana-11_hu_6554adaa7f2ca585.jpg 330w,
        /posts/2023-03-23-grafana/grafana-11_hu_d69df05142764941.jpg 660w,
        /posts/2023-03-23-grafana/grafana-11_hu_14c7d5d413397d81.jpg 1024w,
        /posts/2023-03-23-grafana/grafana-11_hu_370533e3cbc7fe87.jpg 2x"
        data-zoom-src="/posts/2023-03-23-grafana/grafana-11_hu_370533e3cbc7fe87.jpg"
        src="/posts/2023-03-23-grafana/grafana-11_hu_d69df05142764941.jpg"
        alt=""
      />
      
    </figure>
</p>
<p>Укажем источник данных для этого дашборда - наш, уже сконфигурированный <code>Prometheus</code>

    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-03-23-grafana/grafana-12_hu_e13b4c9a1ab076d4.jpg 330w,
        /posts/2023-03-23-grafana/grafana-12_hu_3c27dfd2b933b335.jpg 660w,
        /posts/2023-03-23-grafana/grafana-12_hu_2449b36f1290e007.jpg 1024w,
        /posts/2023-03-23-grafana/grafana-12_hu_635bf3b5e3f0bed.jpg 2x"
        data-zoom-src="/posts/2023-03-23-grafana/grafana-12_hu_635bf3b5e3f0bed.jpg"
        src="/posts/2023-03-23-grafana/grafana-12_hu_3c27dfd2b933b335.jpg"
        alt=""
      />
      
    </figure>
</p>


<h2>Prometheus и Grafana настроили, осталось на сам узел установить Node exporter 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#prometheus-%D0%B8-grafana-%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B8%D0%BB%D0%B8-%D0%BE%D1%81%D1%82%D0%B0%D0%BB%D0%BE%D1%81%D1%8C-%D0%BD%D0%B0-%D1%81%D0%B0%D0%BC-%D1%83%D0%B7%D0%B5%D0%BB-%D1%83%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%B8%D1%82%D1%8C-node-exporter" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Для этого достаточно скачать <a href="https://prometheus.io/download/#node_exporter" target="_blank">бинарный файл</a>, распаковать его и запустить. Все, <code>node-exporter</code> запущен, метрики собираются.</p>


<h2>Результат достигнут 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D1%80%D0%B5%D0%B7%D1%83%D0%BB%D1%8C%D1%82%D0%B0%D1%82-%D0%B4%D0%BE%D1%81%D1%82%D0%B8%D0%B3%D0%BD%D1%83%D1%82" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p><code>Node exporter &lt;--- Prometheus &lt;--- Grafana</code></p>
<p>На узле запущен <code>Node exporter</code>, с него собирает данные <code>Prometheus</code>, а уже из него берет информацию <code>Grafana</code> и отображает в импортированном дашборде.</p>
<p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-03-23-grafana/grafana-13_hu_4d67ce9cc88dc9c0.jpg 330w,
        /posts/2023-03-23-grafana/grafana-13_hu_a24bb98b3a914d3f.jpg 660w,
        /posts/2023-03-23-grafana/grafana-13_hu_33d17e65563de302.jpg 1024w,
        /posts/2023-03-23-grafana/grafana-13_hu_4728807a7b0d0e28.jpg 2x"
        data-zoom-src="/posts/2023-03-23-grafana/grafana-13_hu_4728807a7b0d0e28.jpg"
        src="/posts/2023-03-23-grafana/grafana-13_hu_a24bb98b3a914d3f.jpg"
        alt=""
      />
      
    </figure>
</p>


<h2>Дополнения 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B4%D0%BE%D0%BF%D0%BE%D0%BB%D0%BD%D0%B5%D0%BD%D0%B8%D1%8F" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Помимо <code>Node exporter</code> - у <code>Prometheus</code> огромный выбор и других <a href="https://prometheus.io/docs/instrumenting/exporters/" target="_blank">экспортеров и интеграций</a>. Можно мониторить базы данных, файловые системы, системы логирования и пр.</p>
<p>Например, в нашем файле <code>docker-compose.yml</code> есть сревис <code>cadvisor</code>, он собирает данные по докер контейнерам.</p>
<p>Раскомментируем соответствующую часть</p>
<div><pre tabindex="0"><code data-lang="yml"><span><span><span>  </span><span>cadvisor</span><span>:</span><span>
</span></span></span><span><span><span>    </span><span>image</span><span>:</span><span> </span><span>gcr.io/cadvisor/cadvisor:latest</span><span>
</span></span></span><span><span><span>    </span><span>container_name</span><span>:</span><span> </span><span>cadvisor</span><span>
</span></span></span><span><span><span>    </span><span>ports</span><span>:</span><span>
</span></span></span><span><span><span>    </span>- <span>8080</span><span>:</span><span>8080</span><span>
</span></span></span><span><span><span>    </span><span>volumes</span><span>:</span><span>
</span></span></span><span><span><span>    </span>- <span>/:/rootfs:ro</span><span>
</span></span></span><span><span><span>    </span>- <span>/var/run:/var/run:rw</span><span>
</span></span></span><span><span><span>    </span>- <span>/sys:/sys:ro</span><span>
</span></span></span><span><span><span>    </span>- <span>/var/lib/docker/:/var/lib/docker:ro</span><span>
</span></span></span><span><span><span>    </span><span>depends_on</span><span>:</span><span>
</span></span></span><span><span><span>    </span>- <span>redis</span><span>
</span></span></span><span><span><span>  </span><span>redis</span><span>:</span><span>
</span></span></span><span><span><span>    </span><span>image</span><span>:</span><span> </span><span>redis:latest</span><span>
</span></span></span><span><span><span>    </span><span>container_name</span><span>:</span><span> </span><span>redis</span><span>
</span></span></span><span><span><span>    </span><span>ports</span><span>:</span><span>
</span></span></span><span><span><span>    </span>- <span>6379</span><span>:</span><span>6379</span><span>
</span></span></span></code></pre></div><p>Добавим настройки в конфиг-файл <code>prometheus.yml</code></p>
<div><pre tabindex="0"><code data-lang="yml"><span><span><span>  </span>- <span>job_name</span><span>:</span><span> </span><span>cadvisor</span><span>
</span></span></span><span><span><span>    </span><span>scrape_interval</span><span>:</span><span> </span><span>5s</span><span>
</span></span></span><span><span><span>    </span><span>static_configs</span><span>:</span><span>
</span></span></span><span><span><span>    </span>- <span>targets</span><span>:</span><span>
</span></span></span><span><span><span>      </span>- <span>cadvisor:8080</span><span>
</span></span></span></code></pre></div><p>Перезапустим наши контейнеры</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo docker compose down
</span></span></code></pre></div><div><pre tabindex="0"><code data-lang="bash"><span><span>sudo docker compose up -d
</span></span></code></pre></div><p>В этот раз скачаются и запустятся  еще два контейнера - <code>cadvisor</code> и <code>redis</code></p>
<p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-03-23-grafana/cadvisor-redis_hu_3379655384ed2048.jpg 330w,
        /posts/2023-03-23-grafana/cadvisor-redis_hu_ebdc90560dd2aa9.jpg 660w,
        /posts/2023-03-23-grafana/cadvisor-redis_hu_1a8ee27f73703f21.jpg 1024w,
        /posts/2023-03-23-grafana/cadvisor-redis_hu_95eecb329df50f67.jpg 2x"
        data-zoom-src="/posts/2023-03-23-grafana/cadvisor-redis_hu_95eecb329df50f67.jpg"
        src="/posts/2023-03-23-grafana/cadvisor-redis_hu_ebdc90560dd2aa9.jpg"
        alt=""
      />
      
    </figure>
</p>
<p>Импортируем дашборд для мониторинга контейнеров - например дашборд с ID  179</p>
<p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-03-23-grafana/cadvisor-dashboard_hu_93e70149ca20b00c.jpg 330w,
        /posts/2023-03-23-grafana/cadvisor-dashboard_hu_40ca18be74c1dd86.jpg 660w,
        /posts/2023-03-23-grafana/cadvisor-dashboard_hu_ae762baea3c19c9.jpg 1024w,
        /posts/2023-03-23-grafana/cadvisor-dashboard_hu_3f1eaa589efff67f.jpg 2x"
        data-zoom-src="/posts/2023-03-23-grafana/cadvisor-dashboard_hu_3f1eaa589efff67f.jpg"
        src="/posts/2023-03-23-grafana/cadvisor-dashboard_hu_40ca18be74c1dd86.jpg"
        alt=""
      />
      
    </figure>

Теперь мы можем наблюдать статистику по нашим контейнерам. Сколько каждый контейнер использует памяти или потребляет трафика.</p>


<h2>SNMP 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#snmp" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Хорошо, а что делать с оборудованием на которое лишнего ничего не поставить, роутеры или старые Synology - используем <code>SNMP</code>.</p>
<p>Для примера возьмем роутер <code>Mirkotik</code>, активируем в нем службу <code>SNMP</code></p>
<p><code>WebFig ---&gt; IP ---&gt; SNMP ---&gt; Enabled</code></p>
<p>В нашем файле <code>docker-compose.yml</code> есть контейнер <code>snmp_exporter</code>, он будет собирать данные по <code>SNMP</code> и преобразовавать их в информацию, понятную <code>Prometheus</code>.</p>
<p>Раскомментируем соответствующую часть</p>
<div><pre tabindex="0"><code data-lang="yml"><span><span><span>  </span><span>snmp_exporter</span><span>:</span><span>
</span></span></span><span><span><span>    </span><span>image</span><span>:</span><span> </span><span>prom/snmp-exporter</span><span>
</span></span></span><span><span><span>    </span><span>container_name</span><span>:</span><span> </span><span>snmp_exporter</span><span>
</span></span></span><span><span><span>    </span><span>restart</span><span>:</span><span> </span><span>unless-stopped</span><span>
</span></span></span><span><span><span>    </span><span>volumes</span><span>:</span><span>
</span></span></span><span><span><span>      </span>- <span>./snmp_exporter/:/etc/snmp_exporter/</span><span>
</span></span></span><span><span><span>    </span><span>ports</span><span>:</span><span>
</span></span></span><span><span><span>      </span>- <span>&#34;9116:9116&#34;</span><span>
</span></span></span><span><span><span>    </span><span>depends_on</span><span>:</span><span>
</span></span></span><span><span><span>      </span>- <span>prometheus</span><span>
</span></span></span></code></pre></div><p>Первая буква в <code>SNMP</code> означает <code>Simple</code> - <code>Простой</code>, хотя ничего простого в этом протоколе нет. Каждый производитель использует свои наборы для управления объектами (MIB). В нашем случае мы будем использовать базу, которая подойдет для большинства случаев. Это тот самый файл  <code>snmp.yml</code> в папке <code>snmp_exporter</code>, о котором я не рассказал в самом начале.</p>
<p>Добавим настройки в конфиг-файл <code>prometheus.yml</code></p>
<div><pre tabindex="0"><code data-lang="yml"><span><span><span>  </span>- <span>job_name</span><span>:</span><span> </span><span>&#39;mikrotik&#39;</span><span>
</span></span></span><span><span><span>    </span><span>static_configs</span><span>:</span><span>
</span></span></span><span><span><span>      </span>- <span>targets</span><span>:</span><span>
</span></span></span><span><span><span>        </span>- <span>192.168.88.1</span><span> </span><span># mikrotik_ip</span><span>
</span></span></span><span><span><span>    </span><span>metrics_path</span><span>:</span><span> </span><span>/snmp</span><span>
</span></span></span><span><span><span>    </span><span>params</span><span>:</span><span>
</span></span></span><span><span><span>      </span><span>module</span><span>:</span><span> </span><span>[</span><span>mikrotik]</span><span>
</span></span></span><span><span><span>    </span><span>relabel_configs</span><span>:</span><span>
</span></span></span><span><span><span>      </span>- <span>source_labels</span><span>:</span><span> </span><span>[</span><span>__address__]</span><span>
</span></span></span><span><span><span>        </span><span>target_label</span><span>:</span><span> </span><span>__param_target</span><span>
</span></span></span><span><span><span>      </span>- <span>source_labels</span><span>:</span><span> </span><span>[</span><span>__param_target]</span><span>
</span></span></span><span><span><span>        </span><span>target_label</span><span>:</span><span> </span><span>instance</span><span>
</span></span></span><span><span><span>      </span>- <span>target_label</span><span>:</span><span> </span><span>__address__</span><span>
</span></span></span><span><span><span>        </span><span>replacement</span><span>:</span><span> </span><span>snmp_exporter:9116</span><span>
</span></span></span></code></pre></div><p>Перезапустим наши контейнеры</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo docker compose down
</span></span></code></pre></div><div><pre tabindex="0"><code data-lang="bash"><span><span>sudo docker compose up -d
</span></span></code></pre></div><p>Скачается и запустится еще контейнер - <code>snmp_exporter</code></p>
<p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-03-23-grafana/snmp_exporter_hu_7716de37fce8ba9c.jpg 330w,
        /posts/2023-03-23-grafana/snmp_exporter_hu_f5d0a9e76a8d4534.jpg 660w,
        /posts/2023-03-23-grafana/snmp_exporter_hu_421b1fc291a48477.jpg 1024w,
        /posts/2023-03-23-grafana/snmp_exporter_hu_a595de2c613d53d9.jpg 2x"
        data-zoom-src="/posts/2023-03-23-grafana/snmp_exporter_hu_a595de2c613d53d9.jpg"
        src="/posts/2023-03-23-grafana/snmp_exporter_hu_f5d0a9e76a8d4534.jpg"
        alt=""
      />
      
    </figure>
</p>
<p>Импортируем дашборд для мониторинга контейнеров - мне понравился дашборд с ID  14420</p>


<h2>Mikrotik 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#mikrotik" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-03-23-grafana/mikrotik-dashboard_hu_a218be53437c64ee.jpg 330w,
        /posts/2023-03-23-grafana/mikrotik-dashboard_hu_4cd0da7c7d243be7.jpg 660w,
        /posts/2023-03-23-grafana/mikrotik-dashboard_hu_f8c07d4c7e992fa4.jpg 1024w,
        /posts/2023-03-23-grafana/mikrotik-dashboard_hu_816a279d664dde17.jpg 2x"
        data-zoom-src="/posts/2023-03-23-grafana/mikrotik-dashboard_hu_816a279d664dde17.jpg"
        src="/posts/2023-03-23-grafana/mikrotik-dashboard_hu_4cd0da7c7d243be7.jpg"
        alt=""
      />
      
    </figure>
</p>


<h2>А еще 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B0-%D0%B5%D1%89%D0%B5" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Можно создавать свои графики и настраивать уведомления о событиях. Процесс улучшайзинга графаны - бесконечен.</p>


<h1>Заключение 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B7%D0%B0%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%BD%D0%B8%D0%B5" aria-label="Якорь">#</a>
    </span>        
    
</h1>
<p>Пока создавал статью наткнулся на курсы по grafana/prometheus - стоимостью 20 000 руб.
Поздравляю, вы только что сэкономили эту сумму.</p>
]]></content:encoded></item><item><title>Самоподписной ssl сертификат для внутреннего использования</title><link>https://andrey.dvur.me/posts/2023-02-27-selfsigned-cert/</link><guid isPermaLink="true">https://andrey.dvur.me/posts/2023-02-27-selfsigned-cert/</guid><pubDate>Mon, 27 Feb 2023 00:00:00 +0000</pubDate><category>format-article</category><category>index</category><category>comment-all</category><content:encoded><![CDATA[<p>SSL сертификаты позволяют шифровать данные, от пользователя к веб-сервису. В 2023 все данные в сети передаются зашифрованными, найти сайт с открытой передачей по http(без <strong>s</strong>) уже проблематично и о небезопасности такого сайта первым предупредит браузер.</p>


<h2>Использовать или нет сертификаты (шифрование) внутри своей домашней сети вопрос дискуссионный 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B8%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D1%8C-%D0%B8%D0%BB%D0%B8-%D0%BD%D0%B5%D1%82-%D1%81%D0%B5%D1%80%D1%82%D0%B8%D1%84%D0%B8%D0%BA%D0%B0%D1%82%D1%8B-%D1%88%D0%B8%D1%84%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%B2%D0%BD%D1%83%D1%82%D1%80%D0%B8-%D1%81%D0%B2%D0%BE%D0%B5%D0%B9-%D0%B4%D0%BE%D0%BC%D0%B0%D1%88%D0%BD%D0%B5%D0%B9-%D1%81%D0%B5%D1%82%D0%B8-%D0%B2%D0%BE%D0%BF%D1%80%D0%BE%D1%81-%D0%B4%D0%B8%D1%81%D0%BA%D1%83%D1%81%D1%81%D0%B8%D0%BE%D0%BD%D0%BD%D1%8B%D0%B9" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Я долгое время не заморачивался с ними, поскольку свои сервисы <em>наружу</em> в большой интернет не открываю, а внутри <em>мне скрываться не от кого</em>. Но однажды нашел обсуждение, где один из доводов в защиту сертификатов во внутренней сети было: &ldquo;вы же даете свой пароль wifi гостям, друзьям - значит в вашей сети все-таки появляются <strong>недоверенные</strong> устройства, которые потенциально могут перехватывать открытый трафик&rdquo;.
А когда выяснил насколько легко создать и использовать ssl-сертификаты, вопрос их применения был решен.</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>openssl req -x509 -newkey rsa:4096 -sha256 -days <span>3650</span> -nodes <span>\
</span></span></span><span><span><span></span>  -keyout example.key -out example.crt -subj <span>&#34;/CN=example.com&#34;</span> <span>\
</span></span></span><span><span><span></span>  -addext <span>&#34;subjectAltName=DNS:example.com&#34;</span> 
</span></span></code></pre></div><p>Собственно вот, одна команда и все.
В результате для сайта <code>example.com</code> получим:</p>
<ul>
<li><code>example.key</code> - файл ключа</li>
<li><code>example.crt</code> - файл сертификата</li>
<li>срок действия 10 лет</li>
<li>параметры шифрования <code>rsa:4096 -sha256</code>, что в 2023 являются вменяемыми и достаточными (можете изменить на свое усмотрение)</li>
</ul>
<p>Можно завести в домашней сети <em>свои</em> Yandex.ru или Youtube.com. Но лучшей практикой - для всей домашней сети выбрать один домен, например <code>doma.net</code>, а все домашние сервисы заводить на его поддомены, т.е. <code>youtuba.doma.net</code>, <code>homeassistanta.doma.net</code> и др.</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>openssl req -x509 -newkey rsa:4096 -sha256 -days <span>3650</span> -nodes <span>\
</span></span></span><span><span><span></span>  -keyout domanet.key -out domanet.crt -subj <span>&#34;/CN=*.doma.net&#34;</span> <span>\
</span></span></span><span><span><span></span>  -addext <span>&#34;subjectAltName=DNS:*.doma.net&#34;</span> 
</span></span></code></pre></div><p>Это командой создается ключ и сертификат для <strong>всех</strong> поддоменов <code>*.doma.net</code></p>
<p>Из минусов - все современные браузеры все равно будут ругаться на эти сертификаты и показывать &ldquo;Соединение не защищено&rdquo;, т.к. не могут проверить поставщика (а поставщик мы сами) сертификата. Но шифрование при этом работает.</p>
<p>Да, можно использовать подтвержденные бесплатные сертификаты Lets encrypt, но для них надо:</p>
<ul>
<li>купить доменное имя</li>
<li>скорее всего вы захотите скрыть свои контактные данные, как владельца домена - а это услуга платная</li>
<li>заплатить за перенаправление домена на DNS серверы</li>
<li>заплатить за DNS-хостинг</li>
<li>внести DNS-записи, для перенаправления доменного имени на свой IP-адрес.</li>
<li>Заплатить за статический IP-адрес, либо настроить сервис DDNS</li>
<li>Открыть и пробросить порты 80/443 до каждого сервиса, на который выдается сертификат</li>
<li>Обновлять бесплатный сертификат каждые <strong>3 месяца</strong></li>
</ul>
<p>Бесплатности тут мало, а заморочек много. Сертификаты Lets encrypt я тоже использую, но для внешних сервисов, например - для этого сайта. А для внутренней домашней сети, мне достаточно самоподписных, с коротким и <em>красивым</em> доменом.</p>


<h2>Вывод: использовать сертификаты легко и полезно. 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B2%D1%8B%D0%B2%D0%BE%D0%B4-%D0%B8%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D1%8C-%D1%81%D0%B5%D1%80%D1%82%D0%B8%D1%84%D0%B8%D0%BA%D0%B0%D1%82%D1%8B-%D0%BB%D0%B5%D0%B3%D0%BA%D0%BE-%D0%B8-%D0%BF%D0%BE%D0%BB%D0%B5%D0%B7%D0%BD%D0%BE" aria-label="Якорь">#</a>
    </span>        
    
</h2>
]]></content:encoded></item><item><title>Ищем пожирателя трафика</title><link>https://andrey.dvur.me/posts/2023-02-13-traffic-leak/</link><guid isPermaLink="true">https://andrey.dvur.me/posts/2023-02-13-traffic-leak/</guid><pubDate>Mon, 13 Feb 2023 00:00:00 +0000</pubDate><category>format-article</category><category>index</category><category>comment-all</category><content:encoded><![CDATA[

<h1>Утечка трафика 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D1%83%D1%82%D0%B5%D1%87%D0%BA%D0%B0-%D1%82%D1%80%D0%B0%D1%84%D0%B8%D0%BA%D0%B0" aria-label="Якорь">#</a>
    </span>        
    
</h1>
<p>На одном из серверов случайным образом заметил огромный трафик: за две недели он составил почти 1,5 ТБ, что для сервера с парой телеграм ботов и несколькими микросервисами - перебор.</p>
<p>
    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-02-13-traffic-leak/screen2023-01-16_hu_c4ec31ecd5d0d691.png 330w,
        /posts/2023-02-13-traffic-leak/screen2023-01-16_hu_f5736ca8055d99e4.png 660w,
        /posts/2023-02-13-traffic-leak/screen2023-01-16_hu_a84d561b0a767e6c.png 1024w,
        /posts/2023-02-13-traffic-leak/screen2023-01-16_hu_2f781c7911d63c19.png 2x"
        data-zoom-src="/posts/2023-02-13-traffic-leak/screen2023-01-16_hu_2f781c7911d63c19.png"
        src="/posts/2023-02-13-traffic-leak/screen2023-01-16_hu_f5736ca8055d99e4.png"
        alt=""
      />
      
    </figure>
</p>


<h2>Начинаем искать вредителя. 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%BD%D0%B0%D1%87%D0%B8%D0%BD%D0%B0%D0%B5%D0%BC-%D0%B8%D1%81%D0%BA%D0%B0%D1%82%D1%8C-%D0%B2%D1%80%D0%B5%D0%B4%D0%B8%D1%82%D0%B5%D0%BB%D1%8F" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Сначала проверяю статус файервола (что он активен) и правила. Здесь все в порядке.</p>
<p>Потом смотрю текущие соединения</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>ss -tulpan
</span></span></code></pre></div><p>Подозрительных сессий или открытых портов нет.</p>
<p>Запускаю мониторинг в реальном времени</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>iftop
</span></span></code></pre></div><p>Снова никаких подозрительных действий или потребления трафика, все в рамках сотен Kbps, что никак не может привести к сотням гигабайт. Хорошо, оставляю мониторинг на пару часов и через какое-то время что-то &ldquo;сжирает&rdquo; 80ГБ. Однако iftop только показывает сам факт потребления.</p>
<p>Запускаю другую утилиту, она отсортирует узлы по отправленному трафику в мегабайтах.</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>nethogs -v <span>3</span> -s
</span></span></code></pre></div><p>И снова жду пару часов, как рыбак )</p>
<p>Через пару часов &ldquo;ловлю рыбку&rdquo; в виде ip-адреса 185.56.137.2 с портом 8080.</p>
<p>Смотрю в браузере - бинго!

    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-02-13-traffic-leak/screen2023-01-16-3_hu_2d327cb184eaae8f.png 330w,
        /posts/2023-02-13-traffic-leak/screen2023-01-16-3_hu_8243f0e61d8e91f5.png 660w,
        /posts/2023-02-13-traffic-leak/screen2023-01-16-3_hu_426280bb4cea747e.png 1024w,
        /posts/2023-02-13-traffic-leak/screen2023-01-16-3_hu_fee20507b6185977.png 2x"
        data-zoom-src="/posts/2023-02-13-traffic-leak/screen2023-01-16-3_hu_fee20507b6185977.png"
        src="/posts/2023-02-13-traffic-leak/screen2023-01-16-3_hu_8243f0e61d8e91f5.png"
        alt=""
      />
      
    </figure>
</p>


<h2>Ookla &ldquo;чтоб его&rdquo; Server! 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#ookla-%D1%87%D1%82%D0%BE%D0%B1-%D0%B5%D0%B3%D0%BE-server" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Один из микросервисов на этом сервере - как раз мониторинг скорости внешнего соединения, который каждый час запускает speedtest и строит красивые графики. И вот этот speedtest &ldquo;пожирал&rdquo; десятки гигабайт за тест.</p>
<p>Отключаем speedtest - и вуаля

    <figure>
      <img
       
        loading="lazy"
        srcset="
        /posts/2023-02-13-traffic-leak/screen2023-01-16-2_hu_ffc6f4fb3f95475a.png 330w,
        /posts/2023-02-13-traffic-leak/screen2023-01-16-2_hu_783c55808154a35d.png 660w,
        /posts/2023-02-13-traffic-leak/screen2023-01-16-2_hu_19a07138da838910.png 1024w,
        /posts/2023-02-13-traffic-leak/screen2023-01-16-2_hu_22480d07e98d9f54.png 2x"
        data-zoom-src="/posts/2023-02-13-traffic-leak/screen2023-01-16-2_hu_22480d07e98d9f54.png"
        src="/posts/2023-02-13-traffic-leak/screen2023-01-16-2_hu_783c55808154a35d.png"
        alt=""
      />
      
    </figure>

Объем трафика снизился на порядки.</p>


<h2>Вывод: открыл для себя пару полезных утилит и, да, следить за трафиком - полезно. 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%B2%D1%8B%D0%B2%D0%BE%D0%B4-%D0%BE%D1%82%D0%BA%D1%80%D1%8B%D0%BB-%D0%B4%D0%BB%D1%8F-%D1%81%D0%B5%D0%B1%D1%8F-%D0%BF%D0%B0%D1%80%D1%83-%D0%BF%D0%BE%D0%BB%D0%B5%D0%B7%D0%BD%D1%8B%D1%85-%D1%83%D1%82%D0%B8%D0%BB%D0%B8%D1%82-%D0%B8-%D0%B4%D0%B0-%D1%81%D0%BB%D0%B5%D0%B4%D0%B8%D1%82%D1%8C-%D0%B7%D0%B0-%D1%82%D1%80%D0%B0%D1%84%D0%B8%D0%BA%D0%BE%D0%BC---%D0%BF%D0%BE%D0%BB%D0%B5%D0%B7%D0%BD%D0%BE" aria-label="Якорь">#</a>
    </span>        
    
</h2>
]]></content:encoded></item><item><title>Настройка wireguard сервера</title><link>https://andrey.dvur.me/posts/2023-01-12-wireguard/</link><guid isPermaLink="true">https://andrey.dvur.me/posts/2023-01-12-wireguard/</guid><pubDate>Thu, 12 Jan 2023 00:00:00 +0000</pubDate><category>format-article</category><category>index</category><category>comment-all</category><description>как настроить и подключить впн</description><content:encoded><![CDATA[

<h1>Краткое руководство по настройке wireguard vpn 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%BA%D1%80%D0%B0%D1%82%D0%BA%D0%BE%D0%B5-%D1%80%D1%83%D0%BA%D0%BE%D0%B2%D0%BE%D0%B4%D1%81%D1%82%D0%B2%D0%BE-%D0%BF%D0%BE-%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B5-wireguard-vpn" aria-label="Якорь">#</a>
    </span>        
    
</h1>
<p>Что такое wireguard и зачем он нужен - статей много, здесь ничего этого не будет. Только шаги и команды.</p>
<p><code>Client ---&gt; VPN server ---&gt; Internet</code></p>


<h2>Сервер 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Выбираем физический или виртуальный сервер и подключаемся к консоли</p>
<p>Обновляемся</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo apt update <span>&amp;&amp;</span> sudo apt upgrade -y
</span></span></code></pre></div><p>Устанавливаем сам wireguard</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo apt install wireguard -y
</span></span></code></pre></div><p>Активируем пересылку трафика, для этого редактируем файл</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo nano /etc/sysctl.conf
</span></span></code></pre></div><p>Находим в файле параметр <code>net.ipv4.ip_forward</code>, раскомментируем его и устнавливааем значение <code>=1</code></p>
<p>Проверяем</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>cat /proc/sys/net/ipv4/ip_forward
</span></span></code></pre></div><p>Должна вернутся <code>1</code></p>
<p>Перезагружаемся</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo reboot
</span></span></code></pre></div><p>Подключаемся после перезагрузки</p>
<p>Сгенерируем пару ключей для сервера</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>wg genkey <span>|</span> tee server-pri.key <span>|</span>  wg pubkey &gt; server-pub.key
</span></span></code></pre></div><p>И для клиента</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>wg genkey <span>|</span> tee client-pri.key <span>|</span>  wg pubkey &gt; client-pub.key
</span></span></code></pre></div><p>В резултате получим черыре файла:</p>
<ul>
<li><code>server-pri.key</code> - приватный ключ сервера</li>
<li><code>server-pub.key</code> - публичный ключ сервера</li>
<li><code>client-pri.key</code> - приватный ключ клиента</li>
<li><code>client-pub.key</code> - публичный ключ клиента</li>
</ul>

  
  
  
  



<div
  
   
    style="background-color: #e63946"
  >

  <span
    
     
      style="color: #1d3557"
    >

    

  <span>
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path fill="currentColor"  d="M159.3 5.4c7.8-7.3 19.9-7.2 27.7 .1c27.6 25.9 53.5 53.8 77.7 84c11-14.4 23.5-30.1 37-42.9c7.9-7.4 20.1-7.4 28 .1c34.6 33 63.9 76.6 84.5 118c20.3 40.8 33.8 82.5 33.8 111.9C448 404.2 348.2 512 224 512C98.4 512 0 404.1 0 276.5c0-38.4 17.8-85.3 45.4-131.7C73.3 97.7 112.7 48.6 159.3 5.4zM225.7 416c25.3 0 47.7-7 68.8-21c42.1-29.4 53.4-88.2 28.1-134.4c-2.8-5.6-5.6-11.2-9.8-16.8l-50.6 58.8s-81.4-103.6-87.1-110.6C133.1 243.8 112 273.2 112 306.8C112 375.4 162.6 416 225.7 416z"/></svg>
  </span>


  </span>

  <span
    
      style="color: #f1faee"
    >Приватные ключи должны хранится в надежном месте, публичные - собственно можно публиковать в открытомм доступе.</span>
</div>

<p>Создаем конфигурацию сервера</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo nano /etc/wireguard/wg0.conf
</span></span></code></pre></div><p>Вставляем в него следующее содержимое</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span><span>[</span>Interface<span>]</span>
</span></span><span><span><span># Server</span>
</span></span><span><span><span>Address</span> <span>=</span> 10.10.10.1/24 <span>#адрес нашего виртуального интерфейса</span>
</span></span><span><span><span>ListenPort</span> <span>=</span> <span>51820</span> <span># порт, на котором будет работать wireguard</span>
</span></span><span><span><span>PrivateKey</span> <span>=</span> <span># сюда вставляем содержимое файла server-pri.key, сгенерированного на предыдущем шаге</span>
</span></span><span><span><span>PostUp</span> <span>=</span> iptables -A FORWARD -i wg0 -j ACCEPT<span>;</span> iptables -t nat -A POSTROUTING -o ens1 -j MASQUERADE
</span></span><span><span><span>PostDown</span> <span>=</span> iptables -D FORWARD -i wg0 -j ACCEPT<span>;</span> iptables -t nat -D POSTROUTING -o ens1 -j MASQUERADE
</span></span><span><span><span>#здесь указан физический интерфейс сервера ens1, нобходимо скорректировать под свой сверер</span>
</span></span><span><span>
</span></span><span><span><span>[</span>Peer<span>]</span>
</span></span><span><span><span># Client</span>
</span></span><span><span><span>PublicKey</span> <span>=</span> <span># сюда вставляем содержимое файла client-pub.key, сгенерированного на предыдущем шаге</span>
</span></span><span><span><span>AllowedIPs</span> <span>=</span> 10.10.10.2/32
</span></span></code></pre></div><p>Запускаем сервис wireguard</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo systemctl start wg-quick@wg0
</span></span></code></pre></div>
  



<div
  
   
  >

  <span
    
     
    >

    

  <span>
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z"/></svg>

  </span>


  </span>

  <span
    
     
    >Если сервис wg-quick@wg0 вдруг не запускается - первое, что стоит проверить - присутствует ли в системе пакет resolvconf (если нет - установить sudo apt install resolvconf)</span>
</div>

<p>И устанавливаем автозапуск</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo systemctl <span>enable</span> wg-quick@wg0
</span></span></code></pre></div>

<h2>Клиент 
    
    
    <span
       >
        <a
            style="text-decoration-line: none !important;" href="#%D0%BA%D0%BB%D0%B8%D0%B5%D0%BD%D1%82" aria-label="Якорь">#</a>
    </span>        
    
</h2>
<p>Создаем конфигурацию клиента</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>nano client.conf
</span></span></code></pre></div><p>Вставляем содержимое</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span><span>[</span>Interface<span>]</span>
</span></span><span><span><span>PrivateKey</span> <span>=</span> <span># сюда вставляем содержимое файла client-pri.key</span>
</span></span><span><span><span>Address</span> <span>=</span> 10.10.10.2/32
</span></span><span><span><span>DNS</span> <span>=</span> 1.1.1.1
</span></span><span><span>
</span></span><span><span><span>[</span>Peer<span>]</span>
</span></span><span><span><span>PublicKey</span> <span>=</span> <span># сюда вставляем содержимое файла server-pub.key</span>
</span></span><span><span><span>AllowedIPs</span> <span>=</span> 0.0.0.0/0, ::/0
</span></span><span><span><span>Endpoint</span> <span>=</span> xx.xx.xx.xx:51820 <span># xx.xx.xx.xx внешний адрес нашего сервера</span>
</span></span></code></pre></div><p>Можем либо файлом перенести эту конфигурацию на клиента, либо через QR-код с помощью qrencode</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>sudo apt install qrencode -y
</span></span></code></pre></div><p>Генерируем QR-код либо в отдельный файл</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>cat client.conf  <span>|</span> qrencode -o client.png
</span></span></code></pre></div><p>Либо отображаем QR-код сразу в консоли</p>
<div><pre tabindex="0"><code data-lang="bash"><span><span>qrencode -t ansiutf8 &lt; client.conf
</span></span></code></pre></div><p>Активируем клиента и наслаждаемся.</p>
<p>90% ошибок при настройке - невнимательность. Если что-то не работает - проверяем первым делом ключи и IP-адреса.</p>
]]></content:encoded></item></channel></rss>