Compare commits
28 Commits
79e69c9087
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3837b42437 | |||
| dce89a0380 | |||
| 483b0bbb8c | |||
| b9ed4072f2 | |||
| 4f384a2c79 | |||
| cc21b2b1dc | |||
| 1d070f1f05 | |||
| 6600f2e9c8 | |||
| 67869d7316 | |||
| eb50aec6dc | |||
| 85378102b6 | |||
| 274710ae56 | |||
| bb19a37d62 | |||
| e0c51ea3bd | |||
| 111c1437be | |||
| bd3f6c0b17 | |||
| 44d7546d05 | |||
| 98a80c7cbc | |||
| 4d7b4a744d | |||
| 62bc815a4c | |||
| c131e3ec28 | |||
| 50e5d6cabc | |||
| 9d11029f79 | |||
| e2c15d40b3 | |||
| 8245022322 | |||
| d436eded36 | |||
| bda2762036 | |||
| 72ad195b5a |
@@ -79,13 +79,11 @@ This setup is currently only adapted for Niri.
|
||||
|
||||
## Wallpaper & Colortheme
|
||||
|
||||
The most suitable primary color (or so-called flavor) will be chosen from the [Catppuccin Mocha](https://catppuccin.com/palette/) palette and applied to various apps automatically after changing wallpaper. See also:
|
||||
|
||||
- [wallpaper-carousel](https://github.com/Uyanide/Wallpaper_Carousel): an Image Carousel implemented with Qt Widgets to browse and set wallpapers from.
|
||||
- [wallpaper-daemon](./config/scripts/.local/scripts/wallpaper-daemon): automatic blur (only works in niri).
|
||||
- [change-wallpaper](./config/scripts/.local/scripts/change-wallpaper): script that changes wallpaper with a few extra features.
|
||||
- [change-colortheme](./config/scripts/.local/scripts/change-colortheme): script that changes color scheme based on wallpaper.
|
||||
- [backgrounds collection](https://github.com/Uyanide/backgrounds) for personal use.
|
||||
- [change-colortheme](./config/scripts/.local/scripts/change-colortheme): script that extract colors from the current wallpaper and generate a catppuccin color scheme accordingly.
|
||||
- [backgrounds](https://github.com/Uyanide/backgrounds) collection for personal use (mostly waifus).
|
||||
|
||||
## Rofi
|
||||
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
config/wallpaper/Pictures/backgrounds
|
||||
+2
-1
@@ -30,7 +30,7 @@ GUI_BASE_PKGS = [
|
||||
# "mpv", # media player
|
||||
"wallpaper", # wallpapers & manager
|
||||
"kvantum", # qt theming
|
||||
"nwg-look", # gtk theming
|
||||
"nwg-look" # gtk theming
|
||||
]
|
||||
|
||||
# for Hyprland setup
|
||||
@@ -38,6 +38,7 @@ HYPRLAND_PKGS = [
|
||||
*GUI_BASE_PKGS,
|
||||
"eww", # widgets
|
||||
"hypr", # hypr family
|
||||
"hyprland", # wm config
|
||||
"mako", # notifications
|
||||
"rofi", # application launcher
|
||||
"waybar", # status bar
|
||||
|
||||
+12
-13
@@ -16,7 +16,7 @@ AllowShortFunctionsOnASingleLine: Inline
|
||||
|
||||
AllowShortLambdasOnASingleLine: All
|
||||
|
||||
BreakAfterReturnType: Automatic
|
||||
# BreakAfterReturnType: All
|
||||
|
||||
BinPackArguments: false
|
||||
|
||||
@@ -37,21 +37,20 @@ FixNamespaceComments: true
|
||||
IncludeBlocks: Regroup
|
||||
SortIncludes: true
|
||||
IncludeCategories:
|
||||
- Regex: '^<ext/.*\.h>'
|
||||
Priority: 2
|
||||
- Regex: '^<.*\.h>'
|
||||
Priority: 1
|
||||
- Regex: '^<.*'
|
||||
Priority: 2
|
||||
- Regex: '.*'
|
||||
Priority: 3
|
||||
- Regex: '^<ext/.*\.h>'
|
||||
Priority: 2
|
||||
- Regex: '^<.*\.h>'
|
||||
Priority: 1
|
||||
- Regex: '^<.*'
|
||||
Priority: 2
|
||||
- Regex: '.*'
|
||||
Priority: 3
|
||||
|
||||
IndentWidth: 4
|
||||
|
||||
KeepEmptyLines:
|
||||
AtEndOfFile: true
|
||||
AtStartOfBlock: true
|
||||
AtStartOfFile: false
|
||||
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||
|
||||
InsertNewlineAtEOF: true
|
||||
|
||||
MaxEmptyLinesToKeep: 1
|
||||
|
||||
|
||||
@@ -8,7 +8,13 @@
|
||||
|
||||
file="$HOME/.config/fish/conf.d/60-fetch.fish"
|
||||
|
||||
sed -i -E "s/(set -g fetch_color\s+\"#)([0-9a-fA-F]{6})(\")/\1${colorHex}\3/" "$file" || {
|
||||
# "#rrggbb" format is only available in and after fastfetch v2.42.0,
|
||||
# so we use the equivalent "ANSI Escape Sequences" format for compatibility.
|
||||
# Luckily we have a helper function that can convert something like "rrggbb"
|
||||
# into "\033[38;2;rrr;ggg;bbbm", which was meant to be used in log messages.
|
||||
colorAnsi=$(color_ansi "$colorHex" | sed -E "s/(.\[)([0-9;\]+)(m)/\2/")
|
||||
|
||||
sed -i -E "s/(set -g fetch_color\s+\")([0-9;]+)(\")/\1${colorAnsi}\3/" "$file" || {
|
||||
log_error "Failed to edit ${file}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Vendored
-2
@@ -1,2 +0,0 @@
|
||||
shaders
|
||||
wallpaper*
|
||||
@@ -8,17 +8,18 @@ $entry_border_color = rgba(3B3B3B64)
|
||||
$entry_color = rgba(FFFFFFFF)
|
||||
$font_family = Noto Sans
|
||||
$font_family_clock = Font Awesome 6 Free
|
||||
$font_material_symbols = Material Symbols Rounded
|
||||
$font_family_nerd = Symbols Nerd Font
|
||||
$background = ~/Pictures/backgrounds/miku-space.jpg
|
||||
|
||||
# workaround: https://github.com/hyprwm/hyprlock/issues/825
|
||||
animation = fade, 0
|
||||
# another wierd bug that only happens in hybrid mode :/
|
||||
animation {
|
||||
animation = fade, 0
|
||||
}
|
||||
|
||||
background {
|
||||
# color = rgba(041011FF)
|
||||
# color = rgba(000000FF)
|
||||
# path = {{ SWWW_WALL }}
|
||||
path = ~/Pictures/backgrounds/miku-space.jpg
|
||||
path = $background
|
||||
# blur_size = 5
|
||||
# blur_passes = 4
|
||||
}
|
||||
@@ -68,12 +69,12 @@ label { # Greeting
|
||||
}
|
||||
label { # lock icon
|
||||
monitor =
|
||||
text = lock
|
||||
text =
|
||||
shadow_passes = 1
|
||||
shadow_boost = 0.5
|
||||
color = $text_color
|
||||
font_size = 21
|
||||
font_family = $font_material_symbols
|
||||
font_family = $font_family_nerd
|
||||
|
||||
position = 100, 100
|
||||
halign = left
|
||||
@@ -88,7 +89,7 @@ label { # "locked" text
|
||||
font_size = 14
|
||||
font_family = $font_family
|
||||
|
||||
position = 140, 105
|
||||
position = 140, 100
|
||||
halign = left
|
||||
valign = bottom
|
||||
}
|
||||
|
||||
Executable → Regular
@@ -3,6 +3,7 @@ include "config/monitors.kdl"
|
||||
include "config/styles.kdl"
|
||||
include "config/execs.kdl"
|
||||
include "config/envs.kdl"
|
||||
include "config/prime.kdl"
|
||||
include "config/rules.kdl"
|
||||
include "config/binds.kdl"
|
||||
include "config/misc.kdl"
|
||||
|
||||
@@ -13,10 +13,9 @@ environment {
|
||||
|
||||
// Nvidia
|
||||
LIBVA_DRIVER_NAME "nvidia"
|
||||
__GLX_VENDOR_LIBRARY_NAME "nvidia"
|
||||
NVD_BACKEND "nvidia"
|
||||
GBM_BACKEND "nvidia-drm";
|
||||
WLR_NO_HARDWARE_CURSORS "1";
|
||||
GBM_BACKEND "nvidia-drm"
|
||||
WLR_NO_HARDWARE_CURSORS "1"
|
||||
|
||||
// Fix Swing
|
||||
_JAVA_AWT_WM_NONREPARENTING "1"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
screenshot-path "~/Pictures/Screenshots/niri_screenshot_%Y-%m-%d_%H-%M-%S.png"
|
||||
|
||||
debug {
|
||||
render-drm-device "/dev/dri/renderD129"
|
||||
render-drm-device "/dev/dri/renderD128"
|
||||
}
|
||||
|
||||
// gestures {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
environment {
|
||||
__NV_PRIME_RENDER_OFFLOAD "1"
|
||||
__VK_LAYER_NV_optimus "NVIDIA_only"
|
||||
__GLX_VENDOR_LIBRARY_NAME "nvidia"
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
@import 'colors.css';
|
||||
@import 'colors.css';
|
||||
@@ -1,7 +1,7 @@
|
||||
[Settings]
|
||||
gtk-theme-name=catppuccin-mocha-blue-standard+default
|
||||
gtk-icon-theme-name=Papirus
|
||||
gtk-font-name=Noto Sans, 10
|
||||
gtk-font-name=Noto Sans 10
|
||||
gtk-cursor-theme-name=Bibata-Modern-Ice
|
||||
gtk-cursor-theme-size=24
|
||||
gtk-toolbar-style=GTK_TOOLBAR_ICONS
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[Settings]
|
||||
gtk-theme-name=catppuccin-mocha-blue-standard+default
|
||||
gtk-icon-theme-name=Papirus
|
||||
gtk-font-name=Noto Sans, 10
|
||||
gtk-font-name=Noto Sans 10
|
||||
gtk-cursor-theme-name=Bibata-Modern-Ice
|
||||
gtk-cursor-theme-size=24
|
||||
gtk-application-prefer-dark-theme=1
|
||||
|
||||
@@ -39,7 +39,7 @@ Singleton {
|
||||
} else {
|
||||
Logger.error("IpService", "Failed to fetch IP");
|
||||
}
|
||||
});
|
||||
}, true);
|
||||
}
|
||||
|
||||
function fetchGeoInfo(notify) {
|
||||
|
||||
@@ -4,31 +4,35 @@ import Quickshell.Io
|
||||
import qs.Utils
|
||||
|
||||
Item {
|
||||
// function fakeFetch(resp, callback, forceIPv4 = false) {
|
||||
// if (curlProcess.running) {
|
||||
// Logger.warn("NetworkFetch", "A fetch operation is already in progress.");
|
||||
// return ;
|
||||
// }
|
||||
// fetchedData = "";
|
||||
// fetchingCallback = callback;
|
||||
// curlProcess.command = ["echo", resp];
|
||||
// curlProcess.running = true;
|
||||
// }
|
||||
|
||||
id: root
|
||||
|
||||
property real fetchTimeout: 10 // in seconds
|
||||
property string fetchedData: ""
|
||||
property var fetchingCallback: null
|
||||
|
||||
function fetch(url, callback) {
|
||||
function fetch(url, callback, forceIPv4 = false) {
|
||||
if (curlProcess.running) {
|
||||
Logger.warn("NetworkFetch", "A fetch operation is already in progress.");
|
||||
return ;
|
||||
}
|
||||
fetchedData = "";
|
||||
fetchingCallback = callback;
|
||||
curlProcess.command = ["curl", "-s", "-L", "-m", fetchTimeout.toString(), url];
|
||||
curlProcess.running = true;
|
||||
}
|
||||
curlProcess.command = ["curl", "-s", "-L", "-m", fetchTimeout.toString()];
|
||||
if (forceIPv4)
|
||||
curlProcess.command.push("-4");
|
||||
|
||||
function fakeFetch(resp, callback) {
|
||||
if (curlProcess.running) {
|
||||
Logger.warn("NetworkFetch", "A fetch operation is already in progress.");
|
||||
return ;
|
||||
}
|
||||
fetchedData = "";
|
||||
fetchingCallback = callback;
|
||||
curlProcess.command = ["echo", resp];
|
||||
curlProcess.command.push(url);
|
||||
curlProcess.running = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import argparse
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from threading import Lock
|
||||
|
||||
MAX_WORKERS = 8
|
||||
|
||||
@@ -60,6 +61,10 @@ SCRIPTS = {
|
||||
# or simply `find [-L] <CONFIG_DIR> -type f -name 'apply-color*'` to get all available scripts,
|
||||
# but I need the exact application names anyway, so hardcoding does make some sense
|
||||
|
||||
# A thread-safe counter
|
||||
success_count = 0
|
||||
success_count_lock = Lock()
|
||||
|
||||
|
||||
def hex2rgb(hex_color: str) -> tuple[int, int, int]:
|
||||
"""#rrggbb to (r, g, b)"""
|
||||
@@ -231,6 +236,9 @@ def run_script(script_path: Path, args: list[str]):
|
||||
print(f"Error running {script_path}:\n{result.stderr.strip()}")
|
||||
else:
|
||||
print(f"✓ {script_path}")
|
||||
with success_count_lock:
|
||||
global success_count
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
print(f"Exception running {script_path}: {e}")
|
||||
|
||||
@@ -329,7 +337,7 @@ def main():
|
||||
"-a",
|
||||
"change-colortheme",
|
||||
"Colortheme Changed",
|
||||
f"Palette: {palette_name}\nFlavor: {flavor}\nApplied to {len(apps)} apps",
|
||||
f"Palette: {palette_name}\nFlavor: {flavor}\nApplied to {success_count} apps",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -11,12 +11,13 @@
|
||||
# - change-colortheme (from scripts/change-colortheme)
|
||||
# - flock (usually part of util-linux)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
trap 'echo $LINENO: $BASH_COMMAND' ERR
|
||||
|
||||
# Lock
|
||||
|
||||
exec {LOCK_FD}>/tmp/"$(basename "$0")".lock || {
|
||||
echo "Failed to open lock file"
|
||||
exit 1
|
||||
}
|
||||
exec {LOCK_FD}>/tmp/"$(basename "$0")".lock
|
||||
|
||||
flock -n "$LOCK_FD" || {
|
||||
echo "Another instance is running. Exiting."
|
||||
@@ -26,7 +27,7 @@ flock -n "$LOCK_FD" || {
|
||||
|
||||
# Open a file selection dialog if no argument is provided
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
if [ -z "${1-}" ]; then
|
||||
image=$(zenity --file-selection --title="Open File" --file-filter="*.jpg *.jpeg *.png *.webp *.bmp *.jfif *.tiff *.avif *.heic *.heif")
|
||||
else
|
||||
image="$1"
|
||||
@@ -37,8 +38,8 @@ fi
|
||||
|
||||
# Obtain screen resolution
|
||||
|
||||
screen_width=$2
|
||||
screen_height=$3
|
||||
screen_width=${2-}
|
||||
screen_height=${3-}
|
||||
|
||||
[ -z "$screen_width" ] && {
|
||||
if [ "$XDG_CURRENT_DESKTOP" = "Hyprland" ]; then
|
||||
@@ -56,8 +57,9 @@ screen_height=$3
|
||||
fi
|
||||
}
|
||||
|
||||
[ -z "$screen_width" ] && screen_width=2560
|
||||
[ -z "$screen_height" ] && screen_height=1440
|
||||
## Default to 2k
|
||||
screen_width=${screen_width:-2560}
|
||||
screen_height=${screen_height:-1440}
|
||||
|
||||
# $HOME/.config/wallpaper-chooser/config.json:
|
||||
# ```json
|
||||
@@ -71,37 +73,43 @@ touch "$image" 2>/dev/null || true # ignore errors
|
||||
|
||||
# Copy image to local wallpaper directory
|
||||
|
||||
## Format of current and cached wallpaper
|
||||
wallpaper_ext="png"
|
||||
## Generate a random name for the current wallpaper
|
||||
set +o pipefail # SIGPIPE is expected here
|
||||
random_name=$(tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 16)
|
||||
set -o pipefail
|
||||
## Directory to store current wallpaper
|
||||
current_dir="$HOME/.local/share/wallpaper/current"
|
||||
## Path to current wallpaper image
|
||||
wallpaper_image="$current_dir/wallpaper-${random_name}.${wallpaper_ext}"
|
||||
|
||||
mkdir -p "$current_dir" || {
|
||||
echo "Could not create directory $current_dir"
|
||||
exit 1
|
||||
}
|
||||
mkdir -p "$current_dir"
|
||||
|
||||
temp_img=$(mktemp --suffix=."$wallpaper_ext") || exit 1
|
||||
## Batch copy using a temporary file to avoid incomplete file being used
|
||||
temp_img=$(mktemp --suffix=."$wallpaper_ext")
|
||||
trap 'rm -f "$temp_img"' EXIT
|
||||
magick "$image" -resize "${screen_width}x${screen_height}^" -gravity center -extent "${screen_width}x${screen_height}" "$temp_img" || {
|
||||
echo "Could not resize and crop image"
|
||||
exit 1
|
||||
}
|
||||
cp "$temp_img" "$wallpaper_image" || exit 1
|
||||
magick "$image" -resize "${screen_width}x${screen_height}^" -gravity center -extent "${screen_width}x${screen_height}" "$temp_img"
|
||||
cp "$temp_img" "$wallpaper_image"
|
||||
|
||||
## Generate hash for caching,
|
||||
## based on content of the source image and resolution of the resized image
|
||||
hash="$(md5sum "$image" | awk '{print $1}')-${screen_width}x${screen_height}"
|
||||
|
||||
# Clean up old wallpapers
|
||||
# Clean up old wallpapers in the same directory of current wallpaper.
|
||||
# Only keep the newly added one so the wallpaper-daemon can pick it up directly
|
||||
|
||||
find "$current_dir" -type f -name "wallpaper-*" ! -name "$(basename "$wallpaper_image")" -delete
|
||||
|
||||
# Generate blurred wallpaper
|
||||
|
||||
## Similarly, store blurred version of current wallpaper in separate directory
|
||||
blur_dir="$HOME/.local/share/wallpaper/blurred"
|
||||
## Directory to cache blurred wallpapers, so that we don't need to regenerate
|
||||
## them every time when switching wallpapers. This makes it possible to have
|
||||
## an auto-played slideshow with blurred wallpapers without noticeable delay.
|
||||
blur_cache_dir="$HOME/.local/share/wallpaper/blurred-cache"
|
||||
mkdir -p "$blur_dir" "$blur_cache_dir" || {
|
||||
echo "Could not create cache directory"
|
||||
exit 1
|
||||
}
|
||||
mkdir -p "$blur_dir" "$blur_cache_dir"
|
||||
blurred_image="$blur_dir/blurred-${random_name}.${wallpaper_ext}"
|
||||
blurred_cache_image="$blur_cache_dir/${hash}.${wallpaper_ext}"
|
||||
|
||||
@@ -109,6 +117,14 @@ blurred_cache_image="$blur_cache_dir/${hash}.${wallpaper_ext}"
|
||||
(
|
||||
# notify-send -a "change-wallpaper" "Generating Blurred Wallpaper" "This may take a few seconds..."
|
||||
|
||||
function apply_blured {
|
||||
find "$blur_dir" -type f -name "blurred-*" ! -name "$(basename "$blurred_image")" -delete
|
||||
if [ "$XDG_CURRENT_DESKTOP" = "niri" ]; then
|
||||
swww img -n backdrop "$blurred_image" --transition-type fade --transition-duration 2 >/dev/null 2>/dev/null
|
||||
fi
|
||||
notify-send -a "change-wallpaper" "Blurred Wallpaper Applied" "$blurred_image" -i "$blurred_image"
|
||||
}
|
||||
|
||||
### Check if cached blurred image exists
|
||||
if [ -f "$blurred_cache_image" ]; then
|
||||
# sleep 1 # Some ugly workaround
|
||||
@@ -116,11 +132,7 @@ blurred_cache_image="$blur_cache_dir/${hash}.${wallpaper_ext}"
|
||||
echo "Could not copy cached blurred image"
|
||||
# exit 1 # Non-critical error
|
||||
else
|
||||
find "$blur_dir" -type f -name "blurred-*" ! -name "$(basename "$blurred_image")" -delete
|
||||
if [ "$XDG_CURRENT_DESKTOP" = "niri" ]; then
|
||||
swww img -n backdrop "$blurred_image" --transition-type fade --transition-duration 2 >/dev/null 2>/dev/null
|
||||
fi
|
||||
notify-send -a "change-wallpaper" "Blurred Wallpaper From Cache" "$blurred_image" -i "$blurred_image"
|
||||
apply_blured
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
@@ -133,7 +145,7 @@ blurred_cache_image="$blur_cache_dir/${hash}.${wallpaper_ext}"
|
||||
printf "%.2f", s
|
||||
}')
|
||||
|
||||
### use a temporary file to avoid incomplete file being used
|
||||
### Batch processing using a temporary file to avoid incomplete file being used
|
||||
temp_blurred=$(mktemp --suffix=."$wallpaper_ext") || exit 1
|
||||
trap 'rm -f "${temp_blurred}"' EXIT
|
||||
magick "$wallpaper_image" -blur 0x"$sigma" "$temp_blurred" || {
|
||||
@@ -146,18 +158,12 @@ blurred_cache_image="$blur_cache_dir/${hash}.${wallpaper_ext}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
find "$blur_dir" -type f -name "blurred-*" ! -name "$(basename "$blurred_image")" -delete
|
||||
|
||||
cp -f "$blurred_image" "$blurred_cache_image" || {
|
||||
echo "Could not cache blurred image"
|
||||
# exit 1 # Non-critical error
|
||||
}
|
||||
|
||||
if [ "$XDG_CURRENT_DESKTOP" = "niri" ]; then
|
||||
swww img -n backdrop "$blurred_image" --transition-type fade --transition-duration 2 >/dev/null 2>/dev/null
|
||||
fi
|
||||
|
||||
notify-send -a "change-wallpaper" "Blurred Wallpaper Generated" "$blurred_image" -i "$blurred_image"
|
||||
apply_blured
|
||||
) &
|
||||
|
||||
# Apply wallpaper
|
||||
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Description:
|
||||
# Initialize gpg-agent for SSH support and set up environment variables.
|
||||
# Designed to replace manual ssh-agent management.
|
||||
|
||||
GPG_SSH_SOCKET=$(gpgconf --list-dirs agent-ssh-socket)
|
||||
|
||||
if [ -z "$SSH_AUTH_SOCK" ] || [ "$SSH_AUTH_SOCK" != "$GPG_SSH_SOCKET" ]; then
|
||||
echo "export SSH_AUTH_SOCK='$GPG_SSH_SOCKET';"
|
||||
fi
|
||||
|
||||
# Ensure gpg-agent is aware of the current tty (for passphrase prompts)
|
||||
gpg-connect-agent updatestartuptty /bye > /dev/null 2>&1
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
import termios
|
||||
from select import select
|
||||
from time import time_ns
|
||||
|
||||
QUERY_ID = time_ns() & 0xFFFFFF
|
||||
QUERY_CODE = f"\033_Gi={QUERY_ID},s=1,v=1,a=q,t=d,f=24;AAAA\033\\"
|
||||
EXPECTED_RESPONSE = f"\033_Gi={QUERY_ID};OK\033\\"
|
||||
# Device Attributes request that most terminals respond to
|
||||
FENCE_CODE = "\033[c"
|
||||
FENCE_PATTERN = re.compile(r"\033\[\?.*?c")
|
||||
TIMEOUT = 0.05 # seconds
|
||||
|
||||
# Terminals that are known to support
|
||||
SUPPORTED_TERMINALS = {
|
||||
"xterm-kitty",
|
||||
"xterm-ghostty"
|
||||
}
|
||||
# and those who don't
|
||||
UNSUPPORTED_TERMINALS = {
|
||||
"alacritty",
|
||||
"linux",
|
||||
"kmscon"
|
||||
}
|
||||
# the rest will be tested dynamically
|
||||
|
||||
|
||||
def check_terminal_graphics():
|
||||
if os.environ.get("TERM") in UNSUPPORTED_TERMINALS:
|
||||
return False
|
||||
if os.environ.get("TERM") in SUPPORTED_TERMINALS:
|
||||
return True
|
||||
|
||||
code = QUERY_CODE + FENCE_CODE
|
||||
|
||||
fd = sys.stdin.fileno()
|
||||
if not os.isatty(fd):
|
||||
return False
|
||||
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
response = ""
|
||||
|
||||
try:
|
||||
new_settings = termios.tcgetattr(fd)
|
||||
# Disable canonical mode and echo
|
||||
new_settings[3] = new_settings[3] & ~termios.ICANON & ~termios.ECHO
|
||||
termios.tcsetattr(fd, termios.TCSANOW, new_settings)
|
||||
|
||||
sys.stdout.write(code)
|
||||
sys.stdout.flush()
|
||||
|
||||
while True:
|
||||
# Set a timeout to prevent blocking indefinitely
|
||||
r, w, e = select([fd], [], [], TIMEOUT)
|
||||
if not r:
|
||||
break
|
||||
|
||||
char = os.read(fd, 1)
|
||||
if not char:
|
||||
break
|
||||
|
||||
response += char.decode('utf-8', errors='ignore')
|
||||
|
||||
# If received expected response, return True
|
||||
if EXPECTED_RESPONSE in response:
|
||||
return True
|
||||
|
||||
# If received fence response before any expected response, return False
|
||||
if FENCE_PATTERN.search(response):
|
||||
return False
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSANOW, old_settings)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if check_terminal_graphics():
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
Executable
+88
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
|
||||
def has_ping6() -> bool:
|
||||
try:
|
||||
result = subprocess.run(['ping6', '-c', '1', '::1'],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
return result.returncode == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def ping(host: str, packet_size: int, is_ipv6: bool = False) -> bool:
|
||||
ping6 = has_ping6()
|
||||
if is_ipv6:
|
||||
header_size = 48 # 40 bytes IPv6 header + 8 bytes ICMPv6 header
|
||||
if ping6:
|
||||
cmd = ['ping6', '-c', '2', '-M', 'do', '-s', str(packet_size - header_size), host]
|
||||
else:
|
||||
cmd = ['ping', '-6', '-c', '2', '-M', 'do', '-s', str(packet_size - header_size), host]
|
||||
else:
|
||||
header_size = 28 # 20 bytes IP header + 8 bytes ICMP header
|
||||
if ping6:
|
||||
cmd = ['ping', '-c', '2', '-M', 'do', '-s', str(packet_size - header_size), host]
|
||||
else:
|
||||
cmd = ['ping', '-4', '-c', '2', '-M', 'do', '-s', str(packet_size - header_size), host]
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
return result.returncode == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def bin_search_mtu(host: str, min_mtu: int, max_mtu: int, is_ipv6: bool = False) -> int | None:
|
||||
l = min_mtu
|
||||
h = max_mtu
|
||||
ret: int = 0
|
||||
|
||||
print(f"Testing {'IPv6' if is_ipv6 else 'IPv4'} MTU for {host}...")
|
||||
|
||||
if not ping(host, l, is_ipv6):
|
||||
print(f" Cannot reach host even with MTU {l}.")
|
||||
return None
|
||||
|
||||
while l <= h:
|
||||
m = (l + h) // 2
|
||||
if ping(host, m, is_ipv6):
|
||||
ret = m
|
||||
print(f" MTU {m} OK")
|
||||
l = m + 1
|
||||
else:
|
||||
print(f" MTU {m} Failed")
|
||||
h = m - 1
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2 or len(sys.argv) == 3 or len(sys.argv) > 4:
|
||||
print(f"Usage: {sys.argv[0]} <domain> [min_mtu max_mtu]")
|
||||
sys.exit(1)
|
||||
|
||||
domain: str = sys.argv[1]
|
||||
min_search: int = int(sys.argv[2]) if len(sys.argv) >= 4 else 1000
|
||||
max_search: int = int(sys.argv[3]) if len(sys.argv) >= 4 else 1500
|
||||
|
||||
ipv4_mtu = bin_search_mtu(domain, min_search, max_search, is_ipv6=False)
|
||||
if ipv4_mtu:
|
||||
print(f"Max IPv4 MTU: {ipv4_mtu}")
|
||||
else:
|
||||
print("Could not determine IPv4 MTU.")
|
||||
|
||||
print("-" * 20)
|
||||
|
||||
ipv6_mtu = bin_search_mtu(domain, min_search, max_search, is_ipv6=True)
|
||||
if ipv6_mtu:
|
||||
print(f"Max IPv6 MTU: {ipv6_mtu}")
|
||||
else:
|
||||
print("Could not determine IPv6 MTU.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -2,9 +2,13 @@
|
||||
|
||||
# Description:
|
||||
# Quick snippet of reflector.
|
||||
#
|
||||
# Note:
|
||||
# Write the arguments (with '--' prefix) of reflector to /etc/xdg/reflector/reflector.conf
|
||||
# and enable reflector.timer to update mirrorlist automaticly (once a week)
|
||||
|
||||
[ -z "$COUNTRY" ] && COUNTRY="Germany"
|
||||
[ -z "$COUNTRY" ] && COUNTRY="Germany,Austria"
|
||||
|
||||
sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak || exit 1
|
||||
sudo cp -vf /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak || exit 1
|
||||
|
||||
sudo reflector --country "$COUNTRY" --age 12 --protocol https --sort rate --save /etc/pacman.d/mirrorlist
|
||||
sudo reflector --verbose --country "$COUNTRY" --latest 20 --protocol https --sort rate --save /etc/pacman.d/mirrorlist
|
||||
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
[[ "$XDG_CURRENT_DESKTOP" != "niri" ]] && exit 1
|
||||
|
||||
if grep -q 'prefer_order=(nvidia intel)' "$HOME/.local/snippets/set_display"; then
|
||||
sed -i 's/prefer_order=(nvidia intel)/prefer_order=(intel nvidia)/' "$HOME/.local/snippets/set_display"
|
||||
echo "" >"$HOME/.config/niri/config/prime.kdl"
|
||||
echo "Disabled global Nvidia Prime offloading."
|
||||
else
|
||||
sed -i 's/prefer_order=(intel nvidia)/prefer_order=(nvidia intel)/' "$HOME/.local/snippets/set_display"
|
||||
cat >"$HOME/.config/niri/config/prime.kdl" <<EOF
|
||||
environment {
|
||||
__NV_PRIME_RENDER_OFFLOAD "1"
|
||||
__VK_LAYER_NV_optimus "NVIDIA_only"
|
||||
__GLX_VENDOR_LIBRARY_NAME "nvidia"
|
||||
}
|
||||
EOF
|
||||
echo "Enabled global Nvidia Prime offloading."
|
||||
fi
|
||||
|
||||
# Restart session
|
||||
|
||||
printf "Session restart is required to apply changes.\nDo it now? (Y/n): "
|
||||
read -r answer
|
||||
if [[ "$answer" =~ ^[Yy]$ || -z "$answer" ]]; then
|
||||
niri msg action quit
|
||||
fi
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
# Constants
|
||||
niri_config_file="$HOME/.config/niri/config/misc.kdl"
|
||||
prefer_order=(amd nvidia intel) # AMD -> Nvidia -> Intel
|
||||
prefer_order=(nvidia intel)
|
||||
|
||||
# Get vendor and path of each GPU
|
||||
default_card_path="$(find /dev/dri/card* 2>/dev/null | head -n 1)"
|
||||
@@ -24,49 +24,49 @@ nvidia_render_path=""
|
||||
amd_render_path=""
|
||||
|
||||
for link in /dev/dri/by-path/*-card; do
|
||||
[[ -e "$link" ]] || continue
|
||||
card="$(readlink -f "$link")"
|
||||
vfile="/sys/class/drm/$(basename "$card")/device/vendor"
|
||||
[[ -r "$vfile" ]] || continue
|
||||
vendor="$(cat "$vfile")"
|
||||
case "$vendor" in
|
||||
0x10de) nvidia_card_path="$card" ;;
|
||||
0x8086) intel_card_path="$card" ;;
|
||||
0x1002) amd_card_path="$card" ;;
|
||||
esac
|
||||
[[ -e "$link" ]] || continue
|
||||
card="$(readlink -f "$link")"
|
||||
vfile="/sys/class/drm/$(basename "$card")/device/vendor"
|
||||
[[ -r "$vfile" ]] || continue
|
||||
vendor="$(cat "$vfile")"
|
||||
case "$vendor" in
|
||||
0x10de) nvidia_card_path="$card" ;;
|
||||
0x8086) intel_card_path="$card" ;;
|
||||
0x1002) amd_card_path="$card" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
for link in /dev/dri/by-path/*-render; do
|
||||
[[ -e "$link" ]] || continue
|
||||
render="$(readlink -f "$link")"
|
||||
vfile="/sys/class/drm/$(basename "$render")/device/vendor"
|
||||
[[ -r "$vfile" ]] || continue
|
||||
vendor="$(cat "$vfile")"
|
||||
case "$vendor" in
|
||||
0x10de) nvidia_render_path="$render" ;;
|
||||
0x8086) intel_render_path="$render" ;;
|
||||
0x1002) amd_render_path="$render" ;;
|
||||
esac
|
||||
[[ -e "$link" ]] || continue
|
||||
render="$(readlink -f "$link")"
|
||||
vfile="/sys/class/drm/$(basename "$render")/device/vendor"
|
||||
[[ -r "$vfile" ]] || continue
|
||||
vendor="$(cat "$vfile")"
|
||||
case "$vendor" in
|
||||
0x10de) nvidia_render_path="$render" ;;
|
||||
0x8086) intel_render_path="$render" ;;
|
||||
0x1002) amd_render_path="$render" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Specify device for brightnessctl
|
||||
# Only tested on my laptop with Intel iGPU & Nvidia dGPU
|
||||
BRIGHTNESSCTL_DEVICE="auto"
|
||||
if [[ -n "$intel_card_path" ]]; then
|
||||
BRIGHTNESSCTL_DEVICE="intel_backlight"
|
||||
BRIGHTNESSCTL_DEVICE="intel_backlight"
|
||||
elif [[ -n "$nvidia_card_path" ]]; then
|
||||
BRIGHTNESSCTL_DEVICE="nvidia_0"
|
||||
BRIGHTNESSCTL_DEVICE="nvidia_0"
|
||||
fi
|
||||
export BRIGHTNESSCTL_DEVICE
|
||||
|
||||
# AQ_DRM_DEVICES allows multiple entries separated by colon
|
||||
devices=""
|
||||
for who in "${prefer_order[@]}"; do
|
||||
case "$who" in
|
||||
nvidia) [[ -n "$nvidia_card_path" ]] && devices="${devices:+$devices:}$nvidia_card_path" ;;
|
||||
intel) [[ -n "$intel_card_path" ]] && devices="${devices:+$devices:}$intel_card_path" ;;
|
||||
amd) [[ -n "$amd_card_path" ]] && devices="${devices:+$devices:}$amd_card_path" ;;
|
||||
esac
|
||||
case "$who" in
|
||||
nvidia) [[ -n "$nvidia_card_path" ]] && devices="${devices:+$devices:}$nvidia_card_path" ;;
|
||||
intel) [[ -n "$intel_card_path" ]] && devices="${devices:+$devices:}$intel_card_path" ;;
|
||||
amd) [[ -n "$amd_card_path" ]] && devices="${devices:+$devices:}$amd_card_path" ;;
|
||||
esac
|
||||
done
|
||||
HYPR_AQ_DRM_DEVICES="${devices:-$default_card_path}"
|
||||
export HYPR_AQ_DRM_DEVICES
|
||||
@@ -74,37 +74,37 @@ export HYPR_AQ_DRM_DEVICES
|
||||
# But niri only supports choosing one preferred render device
|
||||
primary_render_device="$default_render_path"
|
||||
for who in "${prefer_order[@]}"; do
|
||||
case "$who" in
|
||||
nvidia) [[ -n "$nvidia_render_path" ]] && {
|
||||
primary_render_device="$nvidia_render_path"
|
||||
break
|
||||
} ;;
|
||||
intel) [[ -n "$intel_render_path" ]] && {
|
||||
primary_render_device="$intel_render_path"
|
||||
break
|
||||
} ;;
|
||||
amd) [[ -n "$amd_render_path" ]] && {
|
||||
primary_render_device="$amd_render_path"
|
||||
break
|
||||
} ;;
|
||||
esac
|
||||
case "$who" in
|
||||
nvidia) [[ -n "$nvidia_render_path" ]] && {
|
||||
primary_render_device="$nvidia_render_path"
|
||||
break
|
||||
} ;;
|
||||
intel) [[ -n "$intel_render_path" ]] && {
|
||||
primary_render_device="$intel_render_path"
|
||||
break
|
||||
} ;;
|
||||
amd) [[ -n "$amd_render_path" ]] && {
|
||||
primary_render_device="$amd_render_path"
|
||||
break
|
||||
} ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Update niri config
|
||||
function update_niri_config() {
|
||||
local config_file="$1"
|
||||
local device_path="$2"
|
||||
local config_file="$1"
|
||||
local device_path="$2"
|
||||
|
||||
[[ -f "$config_file" ]] || return
|
||||
[[ -f "$config_file" ]] || return
|
||||
|
||||
if grep -qE '^\s*render-drm-device\s+"[^"]+"' "$config_file"; then
|
||||
local current
|
||||
current="$(grep -E '^\s*render-drm-device\s+"[^"]+"' "$config_file" | sed -E 's/^\s*render-drm-device\s+"([^"]+)".*/\1/')"
|
||||
[[ "$current" == "$device_path" ]] && return
|
||||
sed -i -E "s|^(\s*render-drm-device\s+)\"[^\"]+\"|\1\"$device_path\"|" "$config_file"
|
||||
else
|
||||
printf '\ndebug {\nrender-drm-device "%s"\n}\n' "$device_path" >>"$config_file"
|
||||
fi
|
||||
if grep -qE '^\s*render-drm-device\s+"[^"]+"' "$config_file"; then
|
||||
local current
|
||||
current="$(grep -E '^\s*render-drm-device\s+"[^"]+"' "$config_file" | sed -E 's/^\s*render-drm-device\s+"([^"]+)".*/\1/')"
|
||||
[[ "$current" == "$device_path" ]] && return
|
||||
sed -i -E "s|^(\s*render-drm-device\s+)\"[^\"]+\"|\1\"$device_path\"|" "$config_file"
|
||||
else
|
||||
printf '\ndebug {\nrender-drm-device "%s"\n}\n' "$device_path" >>"$config_file"
|
||||
fi
|
||||
}
|
||||
|
||||
update_niri_config "$niri_config_file" "$primary_render_device"
|
||||
|
||||
+23
-12
@@ -2,6 +2,8 @@
|
||||
|
||||
# From archlinux's /etc/profile
|
||||
append_path() {
|
||||
[[ -z "$1" ]] && return
|
||||
[[ -d "$1" ]] || return
|
||||
case ":$PATH:" in
|
||||
*:"$1":*) ;;
|
||||
*)
|
||||
@@ -10,6 +12,8 @@ append_path() {
|
||||
esac
|
||||
}
|
||||
prepend_path() {
|
||||
[[ -z "$1" ]] && return
|
||||
[[ -d "$1" ]] || return
|
||||
case ":$PATH:" in
|
||||
*:"$1":*) ;;
|
||||
*)
|
||||
@@ -18,6 +22,9 @@ prepend_path() {
|
||||
esac
|
||||
}
|
||||
|
||||
# .profile is not included in the repo
|
||||
[ -f "$HOME/.profile" ] && . "$HOME/.profile"
|
||||
|
||||
# Better than nothing
|
||||
export XDG_CONFIG_HOME="$HOME/.config"
|
||||
export XDG_DATA_HOME="$HOME/.local/share"
|
||||
@@ -33,29 +40,33 @@ fi
|
||||
|
||||
# Paths
|
||||
[ -f "$HOME/.cargo/env" ] && source "$HOME/.cargo/env"
|
||||
[ -d "$HOME/go/bin" ] && prepend_path "$HOME/go/bin"
|
||||
[ -d "$HOME/.local/bin" ] && prepend_path "$HOME/.local/bin"
|
||||
[ -d "$HOME/.local/scripts" ] && prepend_path "$HOME/.local/scripts"
|
||||
prepend_path "$HOME/go/bin"
|
||||
prepend_path "$HOME/.local/bin"
|
||||
prepend_path "$HOME/.local/scripts"
|
||||
prepend_path "$HOME/.local/share/fnm"
|
||||
export PATH
|
||||
|
||||
# fnm
|
||||
if command -v fnm &>/dev/null; then
|
||||
if type fnm &>/dev/null; then
|
||||
eval $(fnm env --shell bash)
|
||||
fi
|
||||
|
||||
# SSH with cross-session ssh-agent
|
||||
if [ -x "$HOME/.local/scripts/ssh-init" ]; then
|
||||
# export ENABLE_GPG_AGENT_SSH=1 in .profile to enable GPG agent for SSH
|
||||
if [ -x "$HOME/.local/scripts/gpg-init" ] && \
|
||||
[ -n "$ENABLE_GPG_AGENT_SSH" ] && [ "$ENABLE_GPG_AGENT_SSH" != "0" ] && \
|
||||
type gpgconf &>/dev/null; then
|
||||
# GPG agent for SSH
|
||||
eval "$($HOME/.local/scripts/gpg-init 2>/dev/null)" >/dev/null 2>&1
|
||||
elif [ -x "$HOME/.local/scripts/ssh-init" ] && type ssh-agent &>/dev/null; then
|
||||
# SSH with cross-session ssh-agent
|
||||
eval "$($HOME/.local/scripts/ssh-init 2>/dev/null)" >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
# .profile is not included in the repo
|
||||
[ -f "$HOME/.profile" ] && . "$HOME/.profile"
|
||||
|
||||
# Triggered in SSH sessions
|
||||
if [[ $- == *i* ]]; then
|
||||
# Set EDITOR and VISUAL, mainly for sudoedit
|
||||
for app in nvim helix vim vi nano; do
|
||||
if command -v "$app" &>/dev/null; then
|
||||
if type "$app" &>/dev/null; then
|
||||
EDITOR="$app"
|
||||
VISUAL="$app"
|
||||
break
|
||||
@@ -68,9 +79,9 @@ if [[ $- == *i* ]]; then
|
||||
export GPG_TTY
|
||||
|
||||
# Shortcut alias for launching fish
|
||||
command -v f &>/dev/null || {
|
||||
if type fish &>/dev/null && ! type f &>/dev/null; then
|
||||
alias f="exec fish"
|
||||
}
|
||||
fi
|
||||
# Better do this manually since the automatic approach is kinda buggy.
|
||||
# Add this to .bashrc to enable it anyway:
|
||||
#
|
||||
|
||||
+1
@@ -1,5 +1,6 @@
|
||||
*
|
||||
!.gitignore
|
||||
!05-done.fish
|
||||
!10-env.fish
|
||||
!10-niri-env.fish
|
||||
!10-sshs.fish
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
# MIT License
|
||||
|
||||
# Copyright (c) 2016 Francisco Lourenço & Daniel Wehner
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
if not status is-interactive
|
||||
exit
|
||||
end
|
||||
|
||||
set -g __done_version 1.21.1
|
||||
|
||||
function __done_run_powershell_script
|
||||
set -f powershell_exe (command --search "powershell.exe")
|
||||
|
||||
if test "$status" -ne 0
|
||||
and command --search wslvar
|
||||
|
||||
set -f powershell_exe (wslpath (wslvar windir)/System32/WindowsPowerShell/v1.0/powershell.exe)
|
||||
end
|
||||
|
||||
if string length --quiet "$powershell_exe"
|
||||
and test -x "$powershell_exe"
|
||||
|
||||
set cmd (string escape $argv)
|
||||
|
||||
eval "$powershell_exe -Command $cmd"
|
||||
end
|
||||
end
|
||||
|
||||
function __done_windows_notification -a title -a message
|
||||
if test "$__done_notify_sound" -eq 1
|
||||
set soundopt "<audio silent=\"false\" src=\"ms-winsoundevent:Notification.Default\" />"
|
||||
else
|
||||
set soundopt "<audio silent=\"true\" />"
|
||||
end
|
||||
|
||||
__done_run_powershell_script "
|
||||
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
|
||||
[Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
||||
|
||||
\$toast_xml_source = @\"
|
||||
<toast>
|
||||
$soundopt
|
||||
<visual>
|
||||
<binding template=\"ToastText02\">
|
||||
<text id=\"1\">$title</text>
|
||||
<text id=\"2\">$message</text>
|
||||
</binding>
|
||||
</visual>
|
||||
</toast>
|
||||
\"@
|
||||
|
||||
\$toast_xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
||||
\$toast_xml.loadXml(\$toast_xml_source)
|
||||
|
||||
\$toast = New-Object Windows.UI.Notifications.ToastNotification \$toast_xml
|
||||
|
||||
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier(\"fish\").Show(\$toast)
|
||||
"
|
||||
end
|
||||
|
||||
function __done_get_focused_window_id
|
||||
if type -q lsappinfo
|
||||
lsappinfo info -only bundleID (lsappinfo front | string replace 'ASN:0x0-' '0x') | cut -d '"' -f4
|
||||
else if test -n "$SWAYSOCK"
|
||||
and type -q jq
|
||||
swaymsg --type get_tree | jq '.. | objects | select(.focused == true) | .id'
|
||||
else if test -n "$HYPRLAND_INSTANCE_SIGNATURE"
|
||||
hyprctl activewindow | awk 'NR==1 {print $2}'
|
||||
else if test -n "$NIRI_SOCKET"
|
||||
and type -q jq
|
||||
niri msg --json focused-window | jq ".id"
|
||||
else if begin
|
||||
test "$XDG_SESSION_DESKTOP" = gnome; and type -q gdbus
|
||||
end
|
||||
gdbus call --session --dest org.gnome.Shell --object-path /org/gnome/Shell --method org.gnome.Shell.Eval 'global.display.focus_window.get_id()'
|
||||
else if type -q xprop
|
||||
and test -n "$DISPLAY"
|
||||
# Test that the X server at $DISPLAY is running
|
||||
and xprop -grammar >/dev/null 2>&1
|
||||
xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2
|
||||
else if uname -a | string match --quiet --ignore-case --regex microsoft
|
||||
__done_run_powershell_script '
|
||||
Add-Type @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public class WindowsCompat {
|
||||
[DllImport("user32.dll")]
|
||||
public static extern IntPtr GetForegroundWindow();
|
||||
}
|
||||
"@
|
||||
[WindowsCompat]::GetForegroundWindow()
|
||||
'
|
||||
else if set -q __done_allow_nongraphical
|
||||
echo 12345 # dummy value
|
||||
end
|
||||
end
|
||||
|
||||
function __done_is_tmux_window_active
|
||||
set -q fish_pid; or set -l fish_pid %self
|
||||
|
||||
# find the outermost process within tmux
|
||||
# ppid != "tmux" -> pid = ppid
|
||||
# ppid == "tmux" -> break
|
||||
set tmux_fish_pid $fish_pid
|
||||
while set tmux_fish_ppid (ps -o ppid= -p $tmux_fish_pid | string trim)
|
||||
# remove leading hyphen so that basename does not treat it as an argument (e.g. -fish), and return only
|
||||
# the actual command and not its arguments so that basename finds the correct command name.
|
||||
# (e.g. '/usr/bin/tmux' from command '/usr/bin/tmux new-session -c /some/start/dir')
|
||||
and ! string match -q "tmux*" (basename (ps -o command= -p $tmux_fish_ppid | string replace -r '^-' '' | string split ' ')[1])
|
||||
set tmux_fish_pid $tmux_fish_ppid
|
||||
end
|
||||
|
||||
# tmux session attached and window is active -> no notification
|
||||
# all other combinations -> send notification
|
||||
tmux list-panes -a -F "#{session_attached} #{window_active} #{pane_pid}" | string match -q "1 1 $tmux_fish_pid"
|
||||
end
|
||||
|
||||
function __done_is_screen_window_active
|
||||
string match --quiet --regex "$STY\s+\(Attached" (screen -ls)
|
||||
end
|
||||
|
||||
function __done_is_process_window_focused
|
||||
# Return false if the window is not focused
|
||||
|
||||
if set -q __done_allow_nongraphical
|
||||
return 1
|
||||
end
|
||||
|
||||
if set -q __done_kitty_remote_control
|
||||
kitty @ --password="$__done_kitty_remote_control_password" ls | jq -e ".[].tabs[] | select(any(.windows[]; .is_self)) | .is_focused" >/dev/null
|
||||
return $status
|
||||
end
|
||||
|
||||
set __done_focused_window_id (__done_get_focused_window_id)
|
||||
if test "$__done_sway_ignore_visible" -eq 1
|
||||
and test -n "$SWAYSOCK"
|
||||
string match --quiet --regex "^true" (swaymsg -t get_tree | jq ".. | objects | select(.id == "$__done_initial_window_id") | .visible" 2>/dev/null)
|
||||
return $status
|
||||
else if test -n "$HYPRLAND_INSTANCE_SIGNATURE"
|
||||
and test "$__done_initial_window_id" = (hyprctl activewindow | awk 'NR==1 {print $2}')
|
||||
return $status
|
||||
else if test -n "$NIRI_SOCKET"
|
||||
and test "$__done_initial_window_id" = (niri msg --json focused-window | jq ".id")
|
||||
return $status
|
||||
else if test "$__done_initial_window_id" != "$__done_focused_window_id"
|
||||
return 1
|
||||
end
|
||||
# If inside a tmux session, check if the tmux window is focused
|
||||
if type -q tmux
|
||||
and test -n "$TMUX"
|
||||
__done_is_tmux_window_active
|
||||
return $status
|
||||
end
|
||||
|
||||
# If inside a screen session, check if the screen window is focused
|
||||
if type -q screen
|
||||
and test -n "$STY"
|
||||
__done_is_screen_window_active
|
||||
return $status
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
function __done_humanize_duration -a milliseconds
|
||||
set -l seconds (math --scale=0 "$milliseconds/1000" % 60)
|
||||
set -l minutes (math --scale=0 "$milliseconds/60000" % 60)
|
||||
set -l hours (math --scale=0 "$milliseconds/3600000")
|
||||
|
||||
if test "$hours" -gt 0
|
||||
printf '%s' $hours'h '
|
||||
end
|
||||
if test "$minutes" -gt 0
|
||||
printf '%s' $minutes'm '
|
||||
end
|
||||
if test "$seconds" -gt 0
|
||||
printf '%s' $seconds's'
|
||||
end
|
||||
end
|
||||
|
||||
# verify that the system has graphical capabilities before initializing
|
||||
if test -z "$SSH_CLIENT" # not over ssh
|
||||
and count (__done_get_focused_window_id) >/dev/null # is able to get window id
|
||||
set __done_enabled
|
||||
end
|
||||
|
||||
if set -q __done_allow_nongraphical
|
||||
and set -q __done_notification_command
|
||||
set __done_enabled
|
||||
end
|
||||
|
||||
if set -q __done_enabled
|
||||
set -g __done_initial_window_id ''
|
||||
set -q __done_min_cmd_duration; or set -g __done_min_cmd_duration 5000
|
||||
set -q __done_exclude; or set -g __done_exclude '^git (?!push|pull|fetch)'
|
||||
set -q __done_notify_sound; or set -g __done_notify_sound 0
|
||||
set -q __done_sway_ignore_visible; or set -g __done_sway_ignore_visible 0
|
||||
set -q __done_tmux_pane_format; or set -g __done_tmux_pane_format '[#{window_index}]'
|
||||
set -q __done_notification_duration; or set -g __done_notification_duration 3000
|
||||
|
||||
function __done_started --on-event fish_preexec
|
||||
set __done_initial_window_id (__done_get_focused_window_id)
|
||||
end
|
||||
|
||||
function __done_ended --on-event fish_postexec
|
||||
set -l exit_status $status
|
||||
|
||||
# backwards compatibility for fish < v3.0
|
||||
set -q cmd_duration; or set -l cmd_duration $CMD_DURATION
|
||||
|
||||
if test -n "$cmd_duration"
|
||||
and test "$cmd_duration" -gt "$__done_min_cmd_duration" # longer than notify_duration
|
||||
and not __done_is_process_window_focused # process pane or window not focused
|
||||
|
||||
# don't notify if command matches exclude list
|
||||
for pattern in $__done_exclude
|
||||
if string match -qr -- $pattern $argv[1]
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
# Store duration of last command
|
||||
set -l humanized_duration (__done_humanize_duration "$cmd_duration")
|
||||
|
||||
set -l title "Done in $humanized_duration"
|
||||
set -l wd (string replace --regex "^$HOME" "~" (pwd))
|
||||
set -l message "$wd/ $argv[1]"
|
||||
set -l sender $__done_initial_window_id
|
||||
|
||||
if test "$exit_status" -ne 0
|
||||
set title "Failed ($exit_status) after $humanized_duration"
|
||||
end
|
||||
|
||||
if test -n "$TMUX_PANE"
|
||||
set message (tmux lsw -F"$__done_tmux_pane_format" -f '#{==:#{pane_id},'$TMUX_PANE'}')" $message"
|
||||
end
|
||||
|
||||
if set -q __done_notification_command
|
||||
eval $__done_notification_command
|
||||
if test "$__done_notify_sound" -eq 1
|
||||
echo -e "\a" # bell sound
|
||||
end
|
||||
|
||||
else if set -q KITTY_WINDOW_ID
|
||||
printf "\x1b]99;i=done:d=0;$title\x1b\\"
|
||||
printf "\x1b]99;i=done:d=1:p=body;$message\x1b\\"
|
||||
|
||||
else if test "$TERM_PROGRAM" = ghostty; or test "$TERM_PROGRAM" = WezTerm
|
||||
printf "\x1b]777;notify;%s;%s\x1b\\" "$title" "$message"
|
||||
|
||||
else if test "$TERM_PROGRAM" = iTerm.app
|
||||
printf "\x1b]9;%s: %s\x1b\\" "$title" "$message"
|
||||
|
||||
else if type -q terminal-notifier # https://github.com/julienXX/terminal-notifier
|
||||
if test "$__done_notify_sound" -eq 1
|
||||
# pipe message into terminal-notifier to avoid escaping issues (https://github.com/julienXX/terminal-notifier/issues/134). fixes #140
|
||||
# not using the -sender option because it hangs for some apps (https://github.com/julienXX/terminal-notifier/issues/301)
|
||||
echo "$message" | terminal-notifier -title "$title" -sound default
|
||||
else
|
||||
echo "$message" | terminal-notifier -title "$title"
|
||||
end
|
||||
|
||||
else if type -q osascript # AppleScript
|
||||
# escape double quotes that might exist in the message and break osascript. fixes #133
|
||||
set -l message (string replace --all '"' '\"' "$message")
|
||||
set -l title (string replace --all '"' '\"' "$title")
|
||||
|
||||
if test "$__done_notify_sound" -eq 1
|
||||
osascript -e "display notification \"$message\" with title \"$title\" sound name \"Glass\""
|
||||
else
|
||||
osascript -e "display notification \"$message\" with title \"$title\""
|
||||
end
|
||||
|
||||
else if type -q notify-send # Linux notify-send
|
||||
# set urgency to normal
|
||||
set -l urgency normal
|
||||
|
||||
# use user-defined urgency if set
|
||||
if set -q __done_notification_urgency_level
|
||||
set urgency "$__done_notification_urgency_level"
|
||||
end
|
||||
# override user-defined urgency level if non-zero exitstatus
|
||||
if test "$exit_status" -ne 0
|
||||
set urgency critical
|
||||
if set -q __done_notification_urgency_level_failure
|
||||
set urgency "$__done_notification_urgency_level_failure"
|
||||
end
|
||||
end
|
||||
|
||||
notify-send --hint=int:transient:1 --urgency=$urgency --icon=utilities-terminal --app-name=fish --expire-time=$__done_notification_duration "$title" "$message"
|
||||
|
||||
if test "$__done_notify_sound" -eq 1
|
||||
echo -e "\a" # bell sound
|
||||
end
|
||||
|
||||
else if type -q notify-desktop # Linux notify-desktop
|
||||
set -l urgency
|
||||
if test "$exit_status" -ne 0
|
||||
set urgency "--urgency=critical"
|
||||
end
|
||||
notify-desktop $urgency --icon=utilities-terminal --app-name=fish "$title" "$message"
|
||||
if test "$__done_notify_sound" -eq 1
|
||||
echo -e "\a" # bell sound
|
||||
end
|
||||
|
||||
else if uname -a | string match --quiet --ignore-case --regex microsoft
|
||||
__done_windows_notification "$title" "$message"
|
||||
|
||||
else # anything else
|
||||
echo -e "\a" # bell sound
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function __done_uninstall -e done_uninstall
|
||||
# Erase all __done_* functions
|
||||
functions -e __done_ended
|
||||
functions -e __done_started
|
||||
functions -e __done_get_focused_window_id
|
||||
functions -e __done_is_tmux_window_active
|
||||
functions -e __done_is_screen_window_active
|
||||
functions -e __done_is_process_window_focused
|
||||
functions -e __done_windows_notification
|
||||
functions -e __done_run_powershell_script
|
||||
functions -e __done_humanize_duration
|
||||
|
||||
# Erase __done variables
|
||||
set -e __done_version
|
||||
end
|
||||
@@ -3,6 +3,7 @@ fish_add_path $HOME/go/bin
|
||||
fish_add_path $HOME/.cargo/bin
|
||||
fish_add_path $HOME/.local/scripts
|
||||
fish_add_path $HOME/.local/bin
|
||||
fish_add_path $HOME/.local/share/fnm
|
||||
|
||||
# man
|
||||
if type -q bat
|
||||
@@ -28,6 +29,10 @@ end
|
||||
set -x -g GPG_TTY (tty)
|
||||
|
||||
# fnm
|
||||
# if type -q fnm
|
||||
# eval (fnm env --shell fish)
|
||||
# end
|
||||
if type -q fnm
|
||||
eval (fnm env --shell fish)
|
||||
end
|
||||
|
||||
# done
|
||||
set -U __done_min_cmd_duration 10000
|
||||
set -U __done_notification_urgency_level low
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
# ssh with encrypted private keys
|
||||
# $ssh_keys should be set in advance or left empty to use the default keys
|
||||
if type -q ssh
|
||||
|
||||
if set -q ENABLE_GPG_AGENT_SSH; and test $ENABLE_GPG_AGENT_SSH != "0";\
|
||||
and type -q gpg-init; and type -q gpgconf
|
||||
# GPG agent for SSH
|
||||
bass $(gpg-init) > /dev/null 2>&1
|
||||
|
||||
else if type -q ssh-init; and type -q ssh-add
|
||||
# SSH with cross-session ssh-agent
|
||||
bass $(ssh-init) > /dev/null 2>&1
|
||||
|
||||
# avoid entering passphrase every time
|
||||
|
||||
@@ -63,9 +63,9 @@ if type -q zoxide
|
||||
end
|
||||
|
||||
# rm
|
||||
if type -q trash
|
||||
alias rm="echo \"use 'trash' instead :)\" && sh -c \"exit 42\""
|
||||
end
|
||||
# if type -q trash
|
||||
# alias rm="echo \"use 'trash' instead :)\" && sh -c \"exit 42\""
|
||||
# end
|
||||
|
||||
# ls
|
||||
if type -q eza
|
||||
@@ -85,11 +85,54 @@ alias ....='cd ../../..'
|
||||
# grep
|
||||
alias grep="grep --color=auto"
|
||||
|
||||
# others
|
||||
# copy DIR1 DIR2
|
||||
function copy
|
||||
set count (count $argv | tr -d \n)
|
||||
if test "$count" = 2; and test -d "$argv[1]"
|
||||
set from (echo $argv[1] | trim-right /)
|
||||
set to (echo $argv[2])
|
||||
command cp -r $from $to
|
||||
else
|
||||
command cp $argv
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# wget
|
||||
if type -q wget
|
||||
alias wget="wget -c "
|
||||
end
|
||||
|
||||
# colorize
|
||||
alias dir='dir --color=auto'
|
||||
alias vdir='vdir --color=auto'
|
||||
alias grep='grep --color=auto'
|
||||
alias fgrep='fgrep --color=auto'
|
||||
alias egrep='egrep --color=auto'
|
||||
|
||||
# Sort pacman packages by size
|
||||
if type -q expac
|
||||
alias big="expac -H M '%m\t%n' | sort -h | nl"
|
||||
end
|
||||
|
||||
# clock
|
||||
if type -q tty-clock
|
||||
alias clock="tty-clock -c -C 4"
|
||||
end
|
||||
|
||||
# journalctl
|
||||
alias jctl="journalctl -p 3 -xb"
|
||||
|
||||
# nohup
|
||||
function nh
|
||||
nohup $argv >/dev/null 2>&1 & disown
|
||||
end
|
||||
|
||||
# ffmpeg
|
||||
alias ffmpeg="ffmpeg -hide_banner -nostdin"
|
||||
alias ffprobe="ffprobe -hide_banner"
|
||||
|
||||
# git
|
||||
if type -q git
|
||||
function gcp
|
||||
git add -A || return 1
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
if not set -q fetch_logo_type
|
||||
set -g fetch_logo_type auto
|
||||
if type -q kitty-tgp-query; and kitty-tgp-query
|
||||
set -g fetch_logo_type kitty
|
||||
else if type -q kitty-tgp-query
|
||||
set -g fetch_logo_type logo
|
||||
else
|
||||
set -g fetch_logo_type auto
|
||||
end
|
||||
end
|
||||
|
||||
if not set -q fetch_color
|
||||
set -g fetch_color "#89b4fa"
|
||||
set -g fetch_color "38;2;137;180;250"
|
||||
end
|
||||
|
||||
if test "$fetch_logo_type" = symbols
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"wallpaper": {
|
||||
"dirs": [
|
||||
"~/Pictures/backgrounds"
|
||||
"~/Pictures/backgrounds",
|
||||
"/media/Beta/壁纸/库/"
|
||||
],
|
||||
"excludes": [
|
||||
"~/Pictures/backgrounds/nao-stars-crop-adjust-flop.jpg",
|
||||
|
||||
Submodule config/wallpaper/Pictures/backgrounds updated: abde275209...2382384128
@@ -1,11 +1,11 @@
|
||||
[[plugin.deps]]
|
||||
use = "yazi-rs/plugins:git"
|
||||
rev = "68f7d48"
|
||||
rev = "56971d0"
|
||||
hash = "36a484acf6a0a0219c543ccb4cee218f"
|
||||
|
||||
[[plugin.deps]]
|
||||
use = "yazi-rs/plugins:smart-enter"
|
||||
rev = "68f7d48"
|
||||
rev = "56971d0"
|
||||
hash = "56fdabc96fc1f4d53c96eb884b02a5be"
|
||||
|
||||
[[plugin.deps]]
|
||||
|
||||
+17
-13
@@ -1,22 +1,20 @@
|
||||
### Font packages (involved in fontconfig)
|
||||
|
||||
- ttf-sarasa-gothi
|
||||
- ttf-symbola (AUR)
|
||||
- noto-fonts
|
||||
- noto-fonts-cjk
|
||||
- noto-fonts-emoji
|
||||
- ttf-nerd-fonts-symbols
|
||||
- maplemono-nf-cn (AUR)
|
||||
- `extra/ttf-sarasa-gothic`
|
||||
- `aur/ttf-symbola`
|
||||
- `extra/noto-fonts`
|
||||
- `extra/noto-fonts-cjk`
|
||||
- `extra/noto-fonts-emoji`
|
||||
- `extra/ttf-nerd-fonts-symbols`
|
||||
- `aur/maplemono-nf-cn`
|
||||
|
||||
### Other fonts (used but not involved in fontconfig)
|
||||
|
||||
- Sour Gummy
|
||||
- Font Awesome 6 Free
|
||||
- Meslo LGM Nerd Font Mono
|
||||
- Sour Gummy (from [Google Fonts](https://fonts.google.com/specimen/Sour+Gummy))
|
||||
- Font Awesome 6 Free (extracted from an AUR package which no longer exists)
|
||||
- `extra/ttf-meslo-nerd`
|
||||
|
||||
### Fontconfig configuration
|
||||
|
||||
> `~/.config/fontconfig/fonts.conf`
|
||||
### Font configuration
|
||||
|
||||
```xml
|
||||
<?xml version='1.0'?>
|
||||
@@ -82,3 +80,9 @@
|
||||
</alias>
|
||||
</fontconfig>
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- `~/.fonts/` is deprecated, use `~/.local/share/fonts/` instead.
|
||||
|
||||
- `~/.config/fontconfig/fonts.conf` will be loaded by `/etc/fonts/conf.d/50-user.conf` and therefore takes precedence over the rules defined in files starting with a higher number.
|
||||
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
> 虽说更多时候还是防自己, 但是整一套 GPG 密钥体系真的很酷, 多点安全感也绝不是坏事.
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> 仅记录我的折腾过程, 并非指南, 并非推荐, 并非技术文档.
|
||||
|
||||
## What?
|
||||
|
||||
PGP (Pretty Good Privacy) 是一种数据加密和解密的程序, GnuPG (GNU Privacy Guard) 是 PGP 的一个开源实现. GPG, PGP, 真是好名字.
|
||||
|
||||
## How?
|
||||
|
||||
1. 生成密钥对:
|
||||
|
||||
```bash
|
||||
gpg --full-generate-key
|
||||
```
|
||||
|
||||
按照提示选择密钥类型、大小和有效期, 并输入用户信息 (如姓名和电子邮件地址) 和密码.
|
||||
|
||||
2. 列出密钥:
|
||||
|
||||
```bash
|
||||
gpg --list-secret-keys --keyid-format=long
|
||||
```
|
||||
|
||||
从中找到刚刚生成的密钥, 例如:
|
||||
|
||||
```plain
|
||||
sec ed25519/ABCDEF1234567890 2026-01-01 [SC]
|
||||
1234567890ABCDEF1234567890ABCDEF12345678
|
||||
uid [ultimate] Uyanide <email@domain.tld>
|
||||
ssb cv25519/1234567890ABCDEF 2026-01-01 [E]
|
||||
```
|
||||
|
||||
其中 `ABCDEF1234567890` 是主密钥 ID, 记下来.
|
||||
|
||||
3. 生成撤销证书:
|
||||
|
||||
```bash
|
||||
gpg --output revoke.asc --gen-revoke ABCDEF1234567890
|
||||
```
|
||||
|
||||
将 `ABCDEF1234567890` 替换为主密钥 ID. 撤销证书用于在密钥泄露或不再使用时撤销该密钥, 请**离线**妥善保管 `revoke.asc` 文件.
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> **离线** 指的是不连接互联网的环境. 最好将撤销证书存储在物理介质上, 如 USB 驱动器或打印出来, 并放在安全的地方.
|
||||
|
||||
4. 生成子密钥
|
||||
|
||||
GPG 密钥有四种主要功能标志:
|
||||
|
||||
- `C` (Certify): 用于签署其他密钥.
|
||||
- `S` (Sign): 用于签署数据 (如电子邮件或 git 提交).
|
||||
- `E` (Encrypt): 用于加密数据.
|
||||
- `A` (Authenticate): 用于身份验证 (如 SSH).
|
||||
|
||||
主密钥通常只用于 `C` 功能, 默认也会有 `S` 功能. 但是在最佳实践中, 主密钥应只用于 `C` 功能, 其他三种功能由至少三个不同的子密钥承担, 日常使用也多使用子密钥而非主密钥.
|
||||
|
||||
- 对于 `S` 和 `E` 功能, 可以通过以下命令添加子密钥:
|
||||
|
||||
```bash
|
||||
gpg --edit-key ABCDEF1234567890
|
||||
```
|
||||
|
||||
进入交互式界面后, 使用以下命令:
|
||||
|
||||
- `addkey`: 添加子密钥, 按照提示选择密钥类型和大小.
|
||||
- `save`: 保存并退出.
|
||||
|
||||
- 对于 `A` 功能, 可能需要启用 expert 模式:
|
||||
|
||||
> 什么, 专家? 我?
|
||||
|
||||
```bash
|
||||
gpg --expert --edit-key ABCDEF1234567890
|
||||
```
|
||||
|
||||
进入交互式界面后, 使用以下命令:
|
||||
|
||||
1. `addkey`: 添加子密钥
|
||||
2. 选择 `(set your own capabilities)` 后缀的选项作为密钥类型.
|
||||
3. 通过交互式操作仅保留 `A` 功能.
|
||||
4. `Q` 完成密钥功能的设置, 并按照提示选择密钥类型和大小.
|
||||
5. `save`: 保存并退出.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> 和更换麻烦的主密钥相比, 子密钥建议设置为较短的有效期.
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> 即使日常使用不同的子密钥做不同的事, 但实际分享公钥或配置软件时使用的公钥以及密钥 ID 通常仍然是主密钥的公钥和密钥 ID, 而非具体所使用的子密钥的公钥和密钥 ID.
|
||||
|
||||
5. 添加用户 ID
|
||||
|
||||
有些时候需要添加额外的用户 ID (如其他电子邮件地址).
|
||||
|
||||
```bash
|
||||
gpg --edit-key ABCDEF1234567890
|
||||
```
|
||||
|
||||
进入交互式界面后, 使用以下命令:
|
||||
|
||||
- `adduid`: 添加新的用户 ID, 按照提示输入姓名和电子邮件地址.
|
||||
- `save`: 保存并退出.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> 很多平台要求密钥包含在平台经过验证的电子邮件地址, 否则无法使用该密钥进行加密通信.
|
||||
|
||||
6. 导出公钥
|
||||
|
||||
```bash
|
||||
gpg --armor --export ABCDEF1234567890 > publickey.asc
|
||||
```
|
||||
|
||||
将 `ABCDEF1234567890` 替换为主密钥 ID. 这会将公钥导出到 `publickey.asc` 文件中, 可以将其大大方方地分享给他人.
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 每次完成对已有 GPG 密钥的修改(如添加用户 ID, 添加子密钥等)后, 都需要重新导出和更新公钥.
|
||||
|
||||
7. 导出主密钥的私钥
|
||||
|
||||
如果遵循最佳实践, 主密钥的私钥不应该长期保存在本地, 而只在需要时导入使用.
|
||||
|
||||
导出所有私钥到文件:
|
||||
|
||||
```bash
|
||||
gpg --export-secret-keys --armor ABCDEF1234567890 > masterkey.asc
|
||||
gpg --export-secret-subkeys --armor ABCDEF1234567890 > subkeys.asc
|
||||
```
|
||||
|
||||
随后移除本地所有私钥:
|
||||
|
||||
```bash
|
||||
gpg --delete-secret-keys ABCDEF1234567890
|
||||
```
|
||||
|
||||
再导入先前导出的子私钥:
|
||||
|
||||
```bash
|
||||
gpg --import subkeys.asc
|
||||
```
|
||||
|
||||
验证:
|
||||
|
||||
```bash
|
||||
gpg --list-secret-keys --keyid-format=long
|
||||
```
|
||||
|
||||
如果输出的第一行变为 `sec#` 开头, 则表示主密钥的私钥已从 keyring 中移除, 只剩下其 Stub.
|
||||
|
||||
请妥善**离线**保管 `masterkey.asc`, 它是恢复主密钥的唯一途径.
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 与 SSH 密钥不同, GPG 密钥应在多设备间同步, 以便在任何设备上都能向别人验证自己的身份以及进行加密通信.
|
||||
|
||||
8. 设置信任等级
|
||||
|
||||
为了让 GPG 确认密钥是可信的, 有些情况下需要手动为其设置信任等级.
|
||||
|
||||
```bash
|
||||
gpg --edit-key ABCDEF1234567890
|
||||
```
|
||||
|
||||
进入交互式界面后, 使用以下命令:
|
||||
|
||||
- `trust`: 选择 `5 = I trust ultimately` 作为信任等级.
|
||||
- `save`: 保存并退出.
|
||||
|
||||
## Why?
|
||||
|
||||
举个例子, 使用 GPG 密钥对 git 提交进行签名, 参见 Github 文档: [Adding a GPG key to your GitHub account](https://docs.github.com/en/authentication/managing-commit-signature-verification/adding-a-gpg-key-to-your-github-account) 和 [Telling Git about your signing key](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key)
|
||||
|
||||
## One More Thing ...
|
||||
|
||||
通过 gpg-agent, GPG 密钥可以作为 SSH 密钥使用, 这~~真的很酷~~统一了密钥管理的同时也增加了一些安全性. 论使用体验其实目前的 [ssh-init](../config/scripts/.local/scripts/ssh-init) 方案已经足够满足我的要求了, ~~但它真的很酷~~.
|
||||
|
||||
总之, 记录一下折腾过程吧.
|
||||
|
||||
1. 添加认证子密钥
|
||||
|
||||
参考上文, 添加一个仅带有 `A` (Authenticate) 功能的子密钥.
|
||||
|
||||
2. 告诉 GPG 启用 SSH 支持
|
||||
|
||||
编辑 `~/.gnupg/gpg-agent.conf`, 添加或修改以下行:
|
||||
|
||||
```plain
|
||||
enable-ssh-support
|
||||
```
|
||||
|
||||
可选地, 设置缓存有效期 (这对于拥有密码管理系统的桌面来说用处不大):
|
||||
|
||||
```plain
|
||||
default-cache-ttl 600
|
||||
max-cache-ttl 7200
|
||||
```
|
||||
|
||||
3. 绑定密钥
|
||||
|
||||
首先, 获取认证子密钥的 Keygrip:
|
||||
|
||||
```bash
|
||||
gpg --list-secret-keys --with-keygrip ABCDEF1234567890
|
||||
```
|
||||
|
||||
输出类似:
|
||||
|
||||
```plain
|
||||
ssb ed25519 2026-01-16 [A]
|
||||
Keygrip = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
```
|
||||
|
||||
然后将其写入 `~/.gnupg/sshcontrol` 文件中:
|
||||
|
||||
```bash
|
||||
echo "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" >> ~/.gnupg/sshcontrol
|
||||
```
|
||||
|
||||
4. 启动!
|
||||
|
||||
参见 [gpg-init](../config/scripts/.local/scripts/gpg-init) 脚本.
|
||||
|
||||
```bash
|
||||
eval $(gpg-init)
|
||||
```
|
||||
|
||||
此时环境变量 `SSH_AUTH_SOCK` 已指向 gpg-agent 提供的 SSH 代理套接字, 可以像使用普通的 ssh-agent 一样使用 GPG 代理进行 SSH 认证.
|
||||
|
||||
5. 获取公钥
|
||||
|
||||
获取 SSH 公钥以添加到远程服务器或服务:
|
||||
|
||||
```bash
|
||||
ssh-add -L
|
||||
```
|
||||
+4
-2
@@ -46,7 +46,7 @@ ExecStart=/usr/bin/kmscon --vt=%I --seats=seat0 --no-switchvt
|
||||
|
||||
```conf
|
||||
login=/bin/login -p -f kolkas
|
||||
font-name=MesloLGM Nerd Font Mono, Maple Mono NF CN
|
||||
font-name=Maple Mono NF CN
|
||||
font-size=14
|
||||
```
|
||||
|
||||
@@ -93,7 +93,9 @@ login=/usr/bin/su - kolkas -s /usr/bin/fish --login
|
||||
|
||||
### 关于字体的补充说明
|
||||
|
||||
- kmscon 上 2 字符宽的 nerd font 图标会被裁剪至 1 字符宽。可以使用带有 Mono 后缀的 Meslo 系字体作为首选字体,它的图标字符只有 1 字符宽。
|
||||
- ~~kmscon 上 2 字符宽的 nerd font 图标会被裁剪至 1 字符宽。可以使用带有 Mono 后缀的 Meslo 系字体作为首选字体,它的图标字符只有 1 字符宽。~~
|
||||
|
||||
EDIT: kmscon 9.2.0 版本已修复该问题。
|
||||
|
||||
- [Archwiki - KMSCON](https://wiki.archlinux.org/title/KMSCON) 上提供了另一种更改字体的方法,即修改 fontconfig 配置,具体做法为在 monospace 字体族中前置添加字体。但这会影响所有使用 fontconfig 和 monospace 字体族的程序,个人认为并非首选。
|
||||
|
||||
|
||||
+51
-32
@@ -8,17 +8,13 @@
|
||||
|
||||
以下内容按照对应章节划分:
|
||||
|
||||
- [2.4. Creating a New Partition](https://www.linuxfromscratch.org/lfs/view/stable/chapter02/creatingpartition.html)
|
||||
|
||||
虽然此处列举了很多分区,例如 `/opt`,`/usr/sources` 等,但我并不喜欢,也并不觉得有必要将根目录分得如此细碎。对于 UEFI 引导的系统来说(btw 我不认为 BIOS 引导和 UEFI 引导的配置难度之间的差距有很多观点说得那样巨大)除去可选的 Swap 分区外通常 `/`,`/boot`,`/boot/efi`,`/home` 几个分区就足够了。
|
||||
|
||||
- [2.6. Setting the $LFS Variable and the Umask](https://www.linuxfromscratch.org/lfs/view/stable/chapter02/aboutlfs.html)
|
||||
|
||||
`$LFS` 变量很重要!建议写到 Host 的 `/root/.bash_profile` 或 `/etc/profile.d/...` 或类似作用的配置文件里防止忘记设置。
|
||||
|
||||
- [2.7. Mounting the New Partition](https://www.linuxfromscratch.org/lfs/view/stable/chapter02/mounting.html)
|
||||
|
||||
可以用一个小脚本挂载分区,推荐使用文件系统 UUID 而非诸如 `/dev/sdc1` 这样的绝对路径:
|
||||
可以用一个小脚本挂载分区。对于指定分区的方式,推荐使用文件系统 UUID 而非诸如 `/dev/sdc1` 这样的绝对路径:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
@@ -128,9 +124,7 @@
|
||||
它的作用是解压一个只含有一个顶层目录的 tarball,cd 进入解压后得到的目录,生成一个 shell,并在这个 shell 退出时清理先前解压得到的文件。
|
||||
|
||||
另外还有对于在 LFS 与 BLFS 中编译包的一些通用建议:
|
||||
|
||||
1. 通常来说,应该(或者说请务必)在编译和安装一个包后完全删除它的目录,仅有少数例外:
|
||||
|
||||
- linux(保留构建树可以缩短重新构建耗时,或至少保留 config 便于复原配置)
|
||||
- blfs-bootscripts(在 BLFS 中)
|
||||
|
||||
@@ -139,7 +133,6 @@
|
||||
2. 通常来说,建议将所有相关的 tarball 和 patch 下载在同一个目录中,并且将 tarball 也解压在这个目录中。如果目录层级与该默认情况不一致的话必须修改 LFS 书中提供的命令中对应的相对路径。
|
||||
|
||||
3. 如果和我一样使用 UEFI 引导,那么大概率将会在 [8.64. GRUB-2.12](https://www.linuxfromscratch.org/lfs/view/stable/chapter08/grub.html) 第一次接触 BLFS。和 LFS 不同,BLFS 中大多数包都是可选的,一个包可能会依赖其他包,这些依赖分为三个层级:
|
||||
|
||||
- Required
|
||||
- Recommended
|
||||
- Optional
|
||||
@@ -152,7 +145,20 @@
|
||||
|
||||
- [7.4. Entering the Chroot Environment](https://www.linuxfromscratch.org/lfs/view/stable/chapter07/chroot.html)
|
||||
|
||||
对于 chroot 也可以使用一个小脚本显著地改善体验:
|
||||
如果想要使用 `arch-install-scripts` 提供的 `arch-chroot` 脚本偷懒,则必须确保环境变量的清洗与重新设置,至少应确保 `PATH`, `MAKEFLAGS` 和 `TESTSUITEFLAGS` 被正确设置:
|
||||
|
||||
```bash
|
||||
arch-chroot "$LFS" /usr/bin/env -i \
|
||||
HOME=/root \
|
||||
TERM="$TERM" \
|
||||
PS1='(lfs chroot) \u:\w\$ ' \
|
||||
PATH=/usr/local/bin:/usr/bin:/usr/sbin \
|
||||
MAKEFLAGS="-j$(nproc)" \
|
||||
TESTSUITEFLAGS="-j$(nproc)" \
|
||||
/bin/bash --login
|
||||
```
|
||||
|
||||
或者完全按照 LFS 书中的 chroot 步骤编写一个小脚本:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
@@ -200,20 +206,15 @@
|
||||
|
||||
它的作用是自动挂载一系列虚拟文件系统,同时在退出 chroot 时自动清理。
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 此处并不建议使用 `arch-install-scripts` 提供的 `arch-chroot` 偷懒,除非明确知道自己在做什么。
|
||||
|
||||
- [8.64. GRUB-2.12](https://www.linuxfromscratch.org/lfs/view/stable/chapter08/grub.html)
|
||||
|
||||
对于 UEFI 引导的系统,此时需要跳转 BLFS 安装 GRUB。为避免过早地陷入依赖地狱,建议仅按照顺序安装以下包:
|
||||
|
||||
1. [efivar-39](https://www.linuxfromscratch.org/blfs/view/12.4/postlfs/efivar.html)
|
||||
2. [Popt-1.19](https://www.linuxfromscratch.org/blfs/view/12.4/general/popt.html)
|
||||
3. [efibootmgr-18](https://www.linuxfromscratch.org/blfs/view/12.4/postlfs/efibootmgr.html)
|
||||
4. [GRUB-2.12 for EFI](https://www.linuxfromscratch.org/blfs/view/12.4/postlfs/grub-efi.html)
|
||||
|
||||
这对于引导系统来说已经足够用了。如果需要的话可以再之后再补上文档等其他附加选项。
|
||||
这对于引导系统来说已经足够用了。如果需要的话可以之后再补上文档等其他附加依赖重新构建安装。
|
||||
|
||||
- [10.2. Creating the /etc/fstab File](https://www.linuxfromscratch.org/lfs/view/stable/chapter10/fstab.html)
|
||||
|
||||
@@ -226,7 +227,6 @@
|
||||
- [10.3. Linux-6.16.1](https://www.linuxfromscratch.org/lfs/view/stable/chapter10/kernel.html)
|
||||
|
||||
内核配置是整本 LFS 书中最具有挑战的环节。对此我可以总结出几点建议:
|
||||
|
||||
- 复刻并裁剪现有配置
|
||||
|
||||
如果将要使用正在构建的 LFS 系统的机器和 Host 完全相同,并且内核版本相同或相近,可以将 Host 现在运行的内核的配置文件搬过来,同时仅启用当前 Host 加载的内核模块,这将极大地减小配置难度和缩短构建耗时:
|
||||
@@ -253,7 +253,9 @@
|
||||
|
||||
如果内核版本不一致,会在 `make localmodconfig` 时出现很多交互选项,建议全部保持默认,或使用 `make olddefconfig` 来自动处理。
|
||||
|
||||
在此之后,仍建议按照 LFS 书中的指示检查和调整配置选项。
|
||||
对此需要特别注意,大多数成熟发行版的内核都是在有 initrd 的前提下配置、编译和使用的,而在 LFS 中直到 BLFS 才会涉及到 initrd。因跨度过大,不建议直接从这里跳到 BLFS 中配置 initrd。因此需要针对这点手动做一些调整,否则可能无法启动。具体修改方向在之后会提到。
|
||||
|
||||
在此之后,仍建议(或者说请务必)按照 LFS 书中的指示检查和调整配置选项。
|
||||
|
||||
- 参考 Host 的内核配置
|
||||
|
||||
@@ -269,20 +271,22 @@
|
||||
|
||||
- `<*>` or `<M>`
|
||||
|
||||
对于大多数选项,建议选择模块化(M)而非内置(\*)。这将显著减少内核体积,并且提高灵活性。但有几种情况例外,例如:
|
||||
对于大多数选项,建议选择模块化(M)而非内置(\*)。这将显著减少内核体积,并且提高灵活性。但在**没有配置 initrd**(这将在 BLFS 中涉及),有几种情况例外,例如:
|
||||
- 存储总线和控制器驱动,如 `CONFIG_BLK_DEV_NVME`;
|
||||
- RootFS 所需驱动,如 `CONFIG_EXT4_FS`,`CONFIG_BTRFS_FS`;
|
||||
- 如果全盘加密,输入密码(显然)需要键盘或其他输入设备的驱动;
|
||||
- 基础显示驱动,如 `CONFIG_VT`,`CONFIG_VT_CONSOLE` 等。
|
||||
|
||||
- 引导相关的选项(例如 EFI 支持)必须内置,否则无法引导。
|
||||
- 一些必要的文件系统(例如 ext4)建议内置,否则可能无法挂载根文件系统。
|
||||
总之,在挂载 RootFS 之前需要的,以及挂载 RootFS 需要的驱动需要内置。
|
||||
|
||||
也有必须模块化的情况,例如:
|
||||
|
||||
- 需要固件支持的驱动程序(如 i915),除非使用 initrd 或将固件内置到内核中。
|
||||
也有在**没有 initrd** 的情况下必须模块化的情况,例如:
|
||||
- 需要外部固件支持的驱动程序(如 `i915`),除非使用 initrd 或通过 `CONFIG_EXTRA_FIRMWARE` 将固件也内置到内核中。
|
||||
|
||||
- [10.4. Using GRUB to Set Up the Boot Process](https://www.linuxfromscratch.org/lfs/view/stable/chapter10/grub.html)
|
||||
|
||||
强烈建议使用 PARTUUID 和 UUID 替代传统的 `/dev/sdXN` 设备路径以及 `(hdM,N)` 来指定 `/boot` 分区和根分区。
|
||||
|
||||
另外,如果将外置存储设备(如 USB 硬盘)作为根文件系统,建议在 GRUB 配置中添加 `rootdelay=10` 或 `rootwait` 参数以防止启动时找不到根文件系统。
|
||||
另外,如果将外置存储设备(如 USB 硬盘)上的分区作为 RootFS ,建议在 GRUB 配置中添加 `rootdelay=10` 或 `rootwait` 参数以防止启动时找不到 RootFS 。
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
@@ -301,7 +305,6 @@
|
||||
BLFS 并不像 LFS 那样有线性的章节顺序,但仍建议先顺序阅读直到 [After LFS Configuration Issues](https://www.linuxfromscratch.org/blfs/view/stable/postlfs/config.html) 章节**结束**再按自己的需要安装各种包。
|
||||
|
||||
- `su: must be run from a terminal`
|
||||
|
||||
1. What?
|
||||
|
||||
这通常发生在 chroot 后使用 `su` 切换到普通用户,再使用 `su` 试图切换回 root 时。
|
||||
@@ -348,14 +351,14 @@
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
- `-D gallium-drivers=iris,llvmpipe`:
|
||||
- 不包含 NVIDIA 相关的参数,因为 NVIDIA 专有驱动自带完整的 OpenGL 支持,不需要 Mesa 提供。
|
||||
- 同时启用 llvmpipe 用于 OpenGL 上下文中的软件渲染以防万一。
|
||||
- `iris` 用于 Intel 显卡。对于较新(Gen 8 及更新)的硬件,`crocus`(适用于 Gen 4 到 Gen 7)和 `i915`(更老)用户态 OpenGL 驱动已被废弃,不应再使用。注意此处的 i915 和内核中的 i915 内核驱动是不同的东西。
|
||||
- 不包含 NVIDIA 相关的参数,因为 NVIDIA 专有驱动自带完整的 OpenGL 实现,不需要 Mesa 提供;
|
||||
- `iris` 用于现代 Intel 显卡。对于较新(Gen 8 及更新)的硬件,`crocus`(适用于 Gen 4 到 Gen 7.5)和 `i915`(更老)用户态 OpenGL 驱动已被废弃,不应再使用。注意此处的 `i915` 用户态驱动和内核中的 `i915` 模块是不同的东西;
|
||||
- 启用 `llvmpipe` 用于 OpenGL 上下文中的软件渲染以防万一。
|
||||
- `-D vulkan-drivers=intel,swrast`:
|
||||
- 启用 Intel iGPU 的 Vulkan 支持。
|
||||
- 同时启用 Vulkan 上下文中的软件光栅化驱动 swrast 以防万一。
|
||||
- 不用管 NVIDIA,原因同上;
|
||||
- 启用 Intel iGPU 的 Vulkan 支持;
|
||||
- 启用 Vulkan 上下文中的软件渲染驱动“软件光栅化器” `swrast` 以防万一。注意这里的 `swrast` 实际指 `lavapipe`,和被废弃的 gallium `swrast` 驱动是不同的东西。
|
||||
- `-D glvnd=enabled`:启用 GLVND 支持以便和 NVIDIA 专有驱动兼容。libglvnd 需要[在 GLFS 书中安装](https://glfs-book.github.io/glfs/shareddeps/libglvnd.html)。
|
||||
|
||||
> 虽然 GLFS 中的 libglvnd 章节在开头处提到了 `If you've come here from the BLFS Mesa page, ...`,但实际上 BLFS 中的 Mesa 章节并没有提到 libglvnd 和除 nouveau 外与 NVIDIA 相关的话题。算个小坑?大概。
|
||||
@@ -441,8 +444,24 @@
|
||||
|
||||
</details>
|
||||
|
||||
原因是源文件显式包含了多余的 moc 文件,和自动生成的元对象代码冲突,导致重复定义。出问题的源文件有两个,可以通过以下命令修复:
|
||||
原因是源文件显式 include 了多余的 moc 文件,和自动生成的相同作用的代码冲突,导致重复定义。出问题的源文件有两个,可以通过以下命令修复:
|
||||
|
||||
```bash
|
||||
sed -i -E 's|\s*#include "moc_.*|// &|g' qtpositioning/src/plugins/position/geoclue2/qgeopositioninfosourcefactory_geoclue2.cpp qtpositioning/src/plugins/position/geoclue2/qgeopositioninfosource_geoclue2.cpp
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Welcome - Linux From Scratch](https://www.linuxfromscratch.org/)
|
||||
|
||||
- [Linux From Scratch - Version 12.4 ](https://www.linuxfromscratch.org/lfs/view/stable/)
|
||||
|
||||
- [Linux From Scratch - 版本 12.4-中文翻译版](https://lfs.xry111.site/zh_CN/12.4/)
|
||||
|
||||
- [Beyond Linux® From Scratch (System V Edition) - Version 12.4](https://www.linuxfromscratch.org/blfs/view/stable/)
|
||||
|
||||
- [meson.build - Mesa/mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/meson.build)
|
||||
|
||||
- [Intel - Gentoo wiki](https://wiki.gentoo.org/wiki/Intel)
|
||||
|
||||
- [LFS btw](https://io.uyani.de/s/dg7FbrQefPf8sJq)
|
||||
|
||||
@@ -0,0 +1,985 @@
|
||||
> 公共邮箱服务显然已经足够用了,但是用自己的域名收发邮件真的很酷
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> 仅记录我的折腾过程, 并非指南, 并非推荐, 并非技术文档.
|
||||
|
||||
## 目录
|
||||
|
||||
- [目录](#目录)
|
||||
- [概览](#概览)
|
||||
- [一些术语和缩写](#一些术语和缩写)
|
||||
- [要做什么](#要做什么)
|
||||
- [需要什么](#需要什么)
|
||||
- [前置准备](#前置准备)
|
||||
- [放开那个端口!](#放开那个端口)
|
||||
- [注册 SMTP 中继服务](#注册-smtp-中继服务)
|
||||
- [配置 DNS 和 rDNS](#配置-dns-和-rdns)
|
||||
- [在 DNS 服务商处](#在-dns-服务商处)
|
||||
- [在云服务器商处](#在云服务器商处)
|
||||
- [在服务器上](#在服务器上)
|
||||
- [配置邮件服务器](#配置邮件服务器)
|
||||
- [搞定 SSL](#搞定-ssl)
|
||||
- [创建 compose.yaml](#创建-composeyaml)
|
||||
- [创建邮箱账号](#创建邮箱账号)
|
||||
- [配置 SPF](#配置-spf)
|
||||
- [配置 DKIM](#配置-dkim)
|
||||
- [配置 DMARC](#配置-dmarc)
|
||||
- [启动!](#启动)
|
||||
- [Rspamd Web UI](#rspamd-web-ui)
|
||||
- [配置邮件客户端](#配置邮件客户端)
|
||||
- [Thunderbird](#thunderbird)
|
||||
- [Extra Notes](#extra-notes)
|
||||
- [exim4: 我呢?](#exim4-我呢)
|
||||
- [Catch'em All!](#catchem-all)
|
||||
- [MTA-STS](#mta-sts)
|
||||
- [DMARC Alignment](#dmarc-alignment)
|
||||
- [转发到其他域](#转发到其他域)
|
||||
- [查看报告](#查看报告)
|
||||
- [邮件传输链路](#邮件传输链路)
|
||||
- [备份与恢复](#备份与恢复)
|
||||
- [测试工具](#测试工具)
|
||||
- [这一切到底是为什么?](#这一切到底是为什么)
|
||||
|
||||
## 概览
|
||||
|
||||
### 一些术语和缩写
|
||||
|
||||
> 看不懂没关系, 遇到了再回来翻就好 :)
|
||||
|
||||
- M\*A
|
||||
|
||||
| 简写 | 全称 | 中文名称 | 说明 |
|
||||
| ---- | --------------------- | ------------ | ---------------------------------------------------------- |
|
||||
| MTA | Mail Transfer Agent | 邮件传输代理 | 负责在邮件服务器之间传输邮件. 例如 Postfix. |
|
||||
| MDA | Mail Delivery Agent | 邮件投递代理 | 负责将邮件存储到用户邮箱中. 例如 Dovecot. |
|
||||
| MSA | Mail Submission Agent | 邮件提交代理 | 负责接收来自邮件客户端的邮件并将其传递给 MTA. 例如 Postfix |
|
||||
| MUA | Mail User Agent | 邮件用户代理 | 即邮件客户端. 例如 Thunderbird |
|
||||
|
||||
- 协议
|
||||
|
||||
| 简写 | 全称 | 中文名称 | 说明 |
|
||||
| --------------- | -------------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| SMTP | Simple Mail Transfer Protocol | 简单邮件传输协议 | 用于 MTA 之间的服务器通信. 通常运行在 25 端口. |
|
||||
| SMTP Submission | SMTP Submission | SMTP 提交协议 | 用于 MUA 向 MSA 提交邮件. 通常运行在 587 端口, 支持 `STARTTLS` 加密通信. |
|
||||
| SMTPS | SMTP over SSL/TLS | 基于 SSL/TLS 的 SMTP | 用于 MUA 提交邮件给 MSA, 使用独立端口(465)进行加密通信, 支持 `Implicit TLS`, 与 `STARTTLS` 不同的是其在连接建立时便立即进行 SSL/TLS 握手, 降低中间人攻击的风险. |
|
||||
| IMAP | Internet Message Access Protocol | 互联网消息访问协议 | 用于 MUA 从 MDA 拉取邮件. |
|
||||
| IMAPS | IMAP over SSL/TLS | 基于 SSL/TLS 的 IMAP | 使用独立端口(993)进行加密通信. |
|
||||
| POP3 | Post Office Protocol version 3 | 邮局协议版本 3 | 另一种用于 MUA 接收邮件的协议, 不同的是一旦某个 MUA 取走邮件, 该邮件会在服务器上删除, 其他 MUA 将不再能收到这封邮件. 本文不会涉及此协议. |
|
||||
| LMTP | Local Mail Transfer Protocol | 本地邮件传输协议 | 简化的 SMTP, 用于 MTA 在服务器内部将邮件传递给 MDA. |
|
||||
|
||||
- 安全策略
|
||||
|
||||
| 简写 | 全称 | 中文名称 | 说明 |
|
||||
| --------------- | --------------------------------------------------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------- |
|
||||
| rDNS | Reverse DNS | 反向域名解析 | 将 IP 地址映射回域名的特殊 DNS 记录. 多个 ip 可以同时映射到同一个域名, 但一个 ip 只能映射到一个域名. |
|
||||
| PTR | Pointer Record | 指针记录 | rDNS 使用的 DNS 记录类型. |
|
||||
| SPF | Sender Policy Framework | 发件人策略框架 | 用于防止伪造发件人的技术. |
|
||||
| DKIM | DomainKeys Identified Mail | 域名密钥识别邮件 | 用于验证邮件的完整性和真实性的技术. |
|
||||
| DMARC | Domain-based Message Authentication, Reporting, and Conformance | 基于域的消息认证, 报告和一致性 | 用于指定邮件在目标服务器的处理策略. |
|
||||
| DMARC Alignment | DMARC Alignment | DMARC 对齐 | 用于确保发件人域名与 SPF 和 DKIM 认证域名一致的策略. |
|
||||
| MTA-STS | Mail Transfer Agent Strict Transport Security | 邮件传输代理严格传输安全 | 用于防止中间人攻击的技术. |
|
||||
| TLS-RPT | Transport Layer Security Reporting | 传输层安全报告 | 一方向另一方反馈 TLS 相关报告的方式. |
|
||||
| SRS | Sender Rewriting Scheme | 发件人重写方案 | 用于确保在邮件转发后保持 SPF 通过的技术, 通过改写 Envelope From 地址实现. |
|
||||
| ARC | Authenticated Received Chain | 认证接收链 | 用于在邮件转发过程中保持认证信息的技术. |
|
||||
|
||||
- 邮件数据
|
||||
|
||||
| 名称 | 中文名称 | 说明 |
|
||||
| -------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Envelope From | 信封发件人 | 邮件传输过程中使用的发件人地址. 通常在接受到的邮件头部中以 `Return-Path` 字段显示. |
|
||||
| (Header) From | 邮件头部发件人 | 邮件中显示的发件人地址, 也是在邮件客户端中能清晰看到的最醒目的发件人地址. |
|
||||
| Return-Path | 退信路径 | 邮件头部中的一个字段, 用于指定邮件的回执地址, 通常由接受方 MTA 根据 Envelope sender 生成. |
|
||||
| DKIM-Signature | DKIM 签名 | 邮件头部中的一个字段, 包含用于验证邮件完整性和真实性的签名信息. |
|
||||
| Content-Type | 内容类型 | 邮件头部中的一个字段, 用于指定邮件内容的格式和编码方式. 文本为主的邮件通常使用 `text/plain; charset="UTF-8"` 或 `text/html; charset="UTF-8"`, 二者在收件方的垃圾邮件过滤策略中可能会有不同的检测标准和权重. |
|
||||
|
||||
- 反垃圾与声誉
|
||||
|
||||
| 简写 | 全称 | 中文名称 | 说明 |
|
||||
| ---- | ------------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| RBL | Real-time Blackhole List | 实时黑洞列表 | 由第三方维护的黑名单数据库. |
|
||||
| - | Greylisting | 灰名单 | 一种反垃圾邮件技术, 通过拒绝第一次发送的邮件并要求重试来过滤垃圾邮件. |
|
||||
| BIMI | Brand Indicators for Message Identification | 邮件识别的品牌指示符 | 允许在收件人的列表页显示自定义品牌 Logo 的标准. ~~因为我没有品牌也没有 Logo 所以~~本文不会涉及. |
|
||||
|
||||
### 要做什么
|
||||
|
||||
1. 在自己的服务器上配置邮件服务器, 直接接收邮件;
|
||||
|
||||
2. 配置邮件服务器通过 SMTP 中继服务发送邮件;
|
||||
|
||||
> 或者也可以让邮件服务器直接发信, 但要额外做好以下准备:
|
||||
>
|
||||
> - 出入站均畅通无阻的 25 端口
|
||||
>
|
||||
> 大多数云服务商默认封锁 25 端口的出站流量, 需要联系或付费解封.
|
||||
>
|
||||
> - 良好的 IP 声誉
|
||||
>
|
||||
> 可以通过 [mxtoolbox](https://mxtoolbox.com) 或类似网站检查 IP 是否在任意黑名单中. 如果在的话也不是没救, 可以查查对应黑名单的影响范围, 有些其实根本无人在意.
|
||||
>
|
||||
> - 绝对完善的安全策略
|
||||
>
|
||||
> 很多对于使用 SMTP 中继服务时可选的配置在自己发信的场景下是基本要求, 例如 rDNS, DKIM 等. 可以通过 [mail-tester.com](https://www.mail-tester.com) 或 [Hardenize](https://www.hardenize.com/) 等网站检查配置是否完善.
|
||||
>
|
||||
> - 耗费数周乃至数月培养 IP 声誉的时间与物质成本
|
||||
>
|
||||
> 多少沾点玄学. 举个例子, 在这个过程中需要持续向多个公共邮箱平台的多个邮箱发送**有效**邮件, 什么样的邮件是"有效"的? 不知道. 发送多少封才能被认为"无害"? 也不知道. 发送到多少封会被认为"滥用"? 不知道. 不同平台具体采取什么样的评估标准? 还是不知道. 对于纯良的个人邮局来说这无疑是最大的挑战
|
||||
>
|
||||
> - IP 长期保活的持久战准备
|
||||
>
|
||||
> 和上面一点一样也是黑箱. 即使有良好的初始声誉, 如果后续不进行长期维护仍然可能会回到垃圾箱. 这不是短期努力可以解决的, 需要长期坚持.
|
||||
>
|
||||
> 如果确实觉得以上这些都不算问题, 那么可以忽略下文中所有有关 SMTP 中继相关的内容. 其实就本文包含的步骤而言区别并没有很大, 下文中相关的地方会以引用的形式进行标注.
|
||||
|
||||
3. 配置 SPF/DKIM/DMARC/MTA-STS 等等.
|
||||
|
||||
### 需要什么
|
||||
|
||||
1. 一个中意的域名.
|
||||
|
||||
下文中将使用 `domain.tld` 作为示例域名, 使用 `mail.domain.tld` 作为邮件服务器域名, 使用 `me@domain.tld` 作为示例邮箱.
|
||||
|
||||
2. DNS 服务, (至少)需要支持以下记录类型:
|
||||
- A
|
||||
- TXT
|
||||
- MX
|
||||
|
||||
3. SMTP 中继服务.
|
||||
|
||||
也就是帮忙发邮件的服务商, 详见后续章节.
|
||||
|
||||
4. 一个拥有公网 IP 和充足空闲资源的服务器, 并且(至少)需要开通以下 TCP 端口:
|
||||
|
||||
| 端口 | 用途 | 出站 | 入站 | 说明 |
|
||||
| ---- | --------------- | ---- | ---- | --------------------------------------------------- |
|
||||
| 25 | SMTP | ⚠️ | ✅ | 核心传输端口. 如果使用 SMTP 中继服务, 则出站可选 |
|
||||
| 993 | IMAPS | ❌ | ✅ | 用于邮件客户端收信 |
|
||||
| 587 | SMTP Submission | ⚠️ | ✅ | 支持 STARTTLS. 如果使用 SMTP 中继服务, 则也需要出站 |
|
||||
| 465 | SMTPS | ❌ | ✅ | 支持 Implicit SSL/TLS |
|
||||
|
||||
很多云服务商会默认屏蔽 25 端口的出站方向流量, 但这对于使用 SMTP 中继服务的场景来说并不重要, 因为发信时直接连接收件方服务器的并非自己的服务器.
|
||||
|
||||
同时, 最好支持 rDNS, 也就是把 IP 反解析到域名. IPv4 和 IPv6 用到哪个就配置哪个, 都用得到就都配置.
|
||||
|
||||
> 如果选择直接发信不使用 SMTP 中继服务, 则必须配置 rDNS
|
||||
|
||||
下文中将使用 `1.14.5.14` 作为服务器的公网 IP 地址.
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 在网络工程和状态防火墙的语境中, 当提到"封锁端口 X 的出站流量"时, 通常指的是"禁止从本地服务器发起到外部服务器的连接请求, 目标端口为 X". 相反, "封锁端口 X 的入站流量"则意味着"禁止外部服务器发起到本地服务器的连接请求, 目标端口为 X". 策略中用于识别流量的是目标端口, 而非源端口. 例如即使"25 端口的出站流量被封", 如果强行用 25 端口连接外部服务器的其他开放端口如 80, 这种连接请求仍然是允许的. 当然策略中也可以通过源端口进行过滤, 不过这么做通常意义不大.
|
||||
|
||||
## 前置准备
|
||||
|
||||
### 放开那个端口!
|
||||
|
||||
1. 检测 25 端口是否真的开放:
|
||||
|
||||
在服务器上运行:
|
||||
|
||||
```bash
|
||||
sudo nc -l -p 25
|
||||
```
|
||||
|
||||
在另一台机器上运行:
|
||||
|
||||
```bash
|
||||
nc -vz <服务器公网IP> 25
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> btw, 如果想测 25 端口出站是否被封, 可以借助类似 gmail 这样的公共邮箱服务测试. 先运行:
|
||||
>
|
||||
> ```bash
|
||||
> host -t mx gmail.com
|
||||
> ```
|
||||
>
|
||||
> 这会输出 google 的 SMTP 服务器地址, 选一个即可, 例如 `smtp.gmail.com`. 然后运行:
|
||||
>
|
||||
> ```bash
|
||||
> nc -vz smtp.gmail.com 25
|
||||
> ```
|
||||
>
|
||||
> 如果显示 Success 就没问题, 反之如果报错或超时, 那么说明 25 端口的出站被封了.
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 再次说明, 如果使用 SMTP 中继服务, 25 端口的出站并不重要. 只需要保证入站开放即可. 反之则必须保证 25 端口出入站通畅.
|
||||
|
||||
2. 解决占用:
|
||||
|
||||
我的服务器是 Debian 13 系统, 默认使用 `exim4` 作为邮件传输代理(MTA). 不出意外的话 25 端口已经在它手里攥了很久了. 因此除了在防火墙里放行 25 端口外, 还需要禁用 `exim4`.
|
||||
|
||||
```bash
|
||||
sudo systemctl disable --now exim4
|
||||
```
|
||||
|
||||
如果确实需要系统内部通信, 例如 `cron` 发送邮件通知, 可以安装 `ssmtp` 或 `msmtp` 之类的轻量级 MTA, 参见 [后续章节](#exim4-我呢).
|
||||
|
||||
### 注册 SMTP 中继服务
|
||||
|
||||
> 如果选择直接发信不使用 SMTP 中继服务, 则**跳过**本节内容.
|
||||
|
||||
此类服务可以大致理解为"帮忙发邮件的中介", 他们通常有很多 IP 地址, 这些地址的声誉都不错, 配置也很完善, 因此用他们发信的话, 邮件更容易送达收件箱而不是自动被扔进垃圾箱. 并且这也可以避免 25 端口出站被封的问题.
|
||||
|
||||
我此次用的是 [Resend](https://resend.com), 其他类似服务还有 Amazon SES, Mailgun 等等.
|
||||
|
||||
大体分为这样几步:
|
||||
|
||||
1. 注册账号, 并完成邮箱验证.
|
||||
|
||||
2. 添加发信域名, 并获取 DNS 记录值.
|
||||
|
||||
3. 在 DNS 服务商处添加相应的 DNS 记录.
|
||||
|
||||
4. 回到 SMTP 服务商处完成域名验证.
|
||||
|
||||
5. 创建 API Key 或 SMTP 凭据.
|
||||
|
||||
## 配置 DNS 和 rDNS
|
||||
|
||||
### 在 DNS 服务商处
|
||||
|
||||
除了上述 SMTP 服务商提供的 DNS 记录外, 还需要添加以下记录:
|
||||
|
||||
- MX 记录:
|
||||
- 主机名: `@`
|
||||
- 值: `mail.domain.tld`
|
||||
- 优先级: `10`
|
||||
|
||||
这将会是邮件的接收和发送服务器的域名.
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> 如果乐意的话可以把收信域名, 发信域名, 乃至退信域名等等都拆分开来配置, 但这超过了本文的讨论范围且配置大同小异, 因此不做另外说明 ~~主要是懒~~ :)
|
||||
>
|
||||
> 在使用 SMTP 中继服务的情况下, 这个域名只作为收信服务器存在, 发信时使用的域名通常是 SMTP 服务商提供的子域名.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> 优先级数值越小, 优先级越高. 如果同一个域名下有多个 MX 记录, 那么邮件服务器会优先选择优先级最高的记录进行连接.
|
||||
|
||||
- A 记录:
|
||||
- 主机名: `mail`
|
||||
- 值: `1.14.5.14`
|
||||
|
||||
指向邮件服务器的公网 IP 地址.
|
||||
|
||||
其他记录将会在启动邮件服务器后配置.
|
||||
|
||||
### 在云服务器商处
|
||||
|
||||
将服务器 ip 的 rDNS 设置为 `mail.domain.tld`. 这对于使用 SMTP 中继服务的场景来说几乎毫无作用, 但是收信时一些发信方也可能会检查 rDNS, 因此最好设置正确, 有备无患.
|
||||
|
||||
> 如果选择直接发信不使用 SMTP 中继服务, 则**必须**配置 rDNS
|
||||
|
||||
### 在服务器上
|
||||
|
||||
同时, `/etc/hosts` 最好也包含 `mail.domain.tld`.
|
||||
|
||||
## 配置邮件服务器
|
||||
|
||||
这里使用 [docker-mailserver](https://docker-mailserver.github.io/docker-mailserver/latest/#welcome-to-the-documentation-for-docker-mailserver) 作为邮件服务器. 当然也可以使用其他的, 但是这个比较简单.
|
||||
|
||||
### 搞定 SSL
|
||||
|
||||
随便什么方法获取包含 `mail.domain.tld` 的 SSL 证书, 放到随便什么记得住的路径下. 我这里直接使用自动续签的泛域名证书了, 放在 `compose.yaml` 同级目录的 `ssl` 目录下, 包含:
|
||||
|
||||
- `fullchain.pem`
|
||||
- `privkey.pem`
|
||||
|
||||
如果使用这种方式, 在续签证书后可能需要重启 `docker-mailserver` 容器以加载新证书, 或者用 `cron` 定期重启容器, 例如一周一次:
|
||||
|
||||
```cron
|
||||
0 3 * * 0 docker restart mailserver
|
||||
```
|
||||
|
||||
或者也可以让 `docker-mailserver` 自己申请证书, 但是我的服务器的 `80` 和 `443` 端口都是 `openresty` 的, 并且恰好有合适的证书, 不想折腾了.
|
||||
|
||||
### 创建 compose.yaml
|
||||
|
||||
```yaml
|
||||
services:
|
||||
mailserver:
|
||||
image: docker.io/mailserver/docker-mailserver:15.1.0
|
||||
container_name: mailserver
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
hostname: mail
|
||||
domainname: domain.tld
|
||||
ports:
|
||||
- '25:25' # SMTP
|
||||
- '143:143' # IMAP
|
||||
- '993:993' # IMAPS
|
||||
- '587:587' # STARTTLS
|
||||
- '465:465' # SMTPS
|
||||
- '127.0.0.1:11334:11334' # Raspamd Web UI
|
||||
environment:
|
||||
- DMS_DEBUG=0
|
||||
# 反垃圾
|
||||
- ENABLE_AMAVIS=0
|
||||
- ENABLE_OPENDKIM=0
|
||||
- ENABLE_OPENDMARC=0
|
||||
- ENABLE_POLICYD_SPF=0
|
||||
- ENABLE_SPAMASSASSIN=0
|
||||
- ENABLE_RSPAMD=1 # 替代上面几个
|
||||
- RSPAMD_LEARN=1 # 自动学习垃圾邮件
|
||||
# 杀毒, 很占资源, 关掉
|
||||
- ENABLE_CLAMAV=0
|
||||
# 防爆破
|
||||
- ENABLE_FAIL2BAN=1
|
||||
# 禁止伪造发件人
|
||||
- SPOOF_PROTECTION=1
|
||||
# 启用 MTA-STS
|
||||
# - ENABLE_MTM_STS=1
|
||||
# 启用 SRS
|
||||
# - ENABLE_SRS=1
|
||||
# 使用自定义证书
|
||||
- SSL_TYPE=manual
|
||||
# 与下方挂载路径对应
|
||||
- SSL_CERT_PATH=/tmp/ssl/fullchain.pem
|
||||
- SSL_KEY_PATH=/tmp/ssl/privkey.pem
|
||||
# 使用 Resend 作为中继
|
||||
- DEFAULT_RELAY_HOST=[smtp.resend.com]:587
|
||||
- RELAY_USER=resend
|
||||
- RELAY_PASSWORD=res_some_random_api_key
|
||||
# 强制使用 ipv4
|
||||
# - POSTFIX_INET_PROTOCOLS=ipv4
|
||||
volumes:
|
||||
- ./maildata:/var/mail
|
||||
- ./mailstate:/var/mail-state
|
||||
- ./maillogs:/var/log/mail
|
||||
- ./config/:/tmp/docker-mailserver/
|
||||
# 反爆破数据持久化
|
||||
- ./fail2ban:/var/lib/fail2ban
|
||||
# 自定义 SSL 证书挂载
|
||||
- ./ssl:/tmp/ssl:ro
|
||||
```
|
||||
|
||||
以上配置中**必须**修改的地方有:
|
||||
|
||||
- `domain.tld`: 替换为真实域名.
|
||||
- `resend`: 替换为在 SMTP 服务商处创建的真实用户名.
|
||||
- `res_some_random_api_key`: 替换为在 SMTP 服务商处创建的 SMTP 凭据或 API Key.
|
||||
|
||||
一些 environment 的解释:
|
||||
|
||||
- 如果机器性能孱弱或很在意占用的资源, 可以关掉 [Rspamd](https://docker-mailserver.github.io/docker-mailserver/latest/config/security/rspamd/) 使用老东西:
|
||||
- `ENABLE_AMAVIS=1`
|
||||
- `ENABLE_OPENDKIM=1`
|
||||
- `ENABLE_OPENDMARC=1`
|
||||
- `ENABLE_POLICYD_SPF=1`
|
||||
- `ENABLE_SPAMASSASSIN=1`
|
||||
- `ENABLE_RSPAMD=0`
|
||||
- `RSPAMD_LEARN=0`
|
||||
|
||||
- `RSPAMD_GREYLISTING=0`
|
||||
|
||||
启用后, 在第一次接受陌生人邮件时拒绝, 要求对方重试, 这样可以有效减少垃圾邮件, 但会显著增加延迟.
|
||||
|
||||
- `ENABLE_MTA_STS=1`:
|
||||
|
||||
MTA-STS 可以防止中间人攻击, 但配置较为复杂, [下文](#更进一步) 将单独介绍.
|
||||
|
||||
- `ENABLE_FAIL2BAN=1`:
|
||||
|
||||
Fail2Ban 用于防止暴力破解, 需要 `NET_ADMIN` 权限, 挂载 `./fail2ban` 目录用于保存状态.
|
||||
|
||||
- `DEFAULT_RELAY_HOST=[smtp.resend.com]:587`
|
||||
|
||||
中括号用于跳过 MX 查找直接解析 A 记录, 这对于连接明确的 SMTP 中继服务效率更高且更稳定.
|
||||
|
||||
- `POSTFIX_INET_PROTOCOLS=ipv4`:
|
||||
|
||||
如果服务器的 IPv6 配置不完善, 可以强制 Postfix 仅使用 IPv4. 反之也可以只支持 IPv6.
|
||||
|
||||
- `ENABLE_SRS=1`:
|
||||
|
||||
启用 SRS 用于转发场景, 详见 [后续章节](#转发到其他域).
|
||||
|
||||
> 如果选择直接发信不使用 SMTP 中继服务, **必须**删除以下环境变量:
|
||||
>
|
||||
> - `DEFAULT_RELAY_HOST=[smtp.resend.com]:587`
|
||||
> - `RELAY_USER=resend`
|
||||
> - `RELAY_PASSWORD=res_some_random_api_key`
|
||||
|
||||
如果要进行进一步配置, 必须先启动容器. 此时会报错, 因为还没有创建邮箱账号. 但不用管, 先让它跑着.
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### 创建邮箱账号
|
||||
|
||||
使用 `docker-mailserver` 自带的脚本创建邮箱账号. 例如创建 `me@domain.tld`:
|
||||
|
||||
```bash
|
||||
docker exec -it mailserver setup email add me@domain.tld <密码>
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> 把密码存在 Shell 历史里并不是个好主意. 可以通过在命令的最前面加一个空格来避免保存到历史记录中(具体取决于所使用的 Shell 和其配置). 或者从 stdin 中读取也是个不错的选择.
|
||||
|
||||
其他一些配置命令可以通过 `docker exec -it mailserver setup help` 查看.
|
||||
|
||||
### 配置 SPF
|
||||
|
||||
SPF 记录用于指定哪些服务器被允许代表该域名发送邮件. 它会检查信件 "Envelope sender" (也就是 `Return-Path` 头部) 中的域名是否与对应域名的 SPF 记录匹配, 从而防止伪造发件人.
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 对于使用 SMTP 中继服务的场景, SPF 记录仍然是**必须的**. 即使正常发信时收件方检查的是子域名如 `send.domain.tld` 的 SPF 记录, 但设置根域名 `domain.tld` 的 SPF 记录仍然是良好的实践, 可以防止伪造发件人.
|
||||
|
||||
在 DNS 服务商处添加以下记录:
|
||||
|
||||
- SPF 记录 (TXT 记录):
|
||||
- 主机名: `@`
|
||||
- 值: `v=spf1 -all` (假设使用 Resend 作为 SMTP 服务商)
|
||||
|
||||
解释:
|
||||
- `v=spf1`: 指定 SPF 版本.
|
||||
- `include:resend.com`: 仅允许 Resend 的服务器以此域名的名义发送邮件. 因为 Resend 并不会通过根域名发送邮件, 所以这条记录严格来说可以省略.
|
||||
- `-all`: 硬失败, 未授权的服务器发送邮件时拒绝. 因为发行渠道只有 Resend, 所以这样设置是合理的. 如果希望软失败, 即接受但标记为可疑, 可以使用 `~all`.
|
||||
|
||||
> 如果选择直接发信不使用 SMTP 中继服务, 则**需要**将 `include:resend.com` 替换为自己的邮件服务器的 IP 地址或域名, 例如:
|
||||
>
|
||||
> `v=spf1 a:mail.domain.tld -all`
|
||||
>
|
||||
> 详细语法参见 [SPF 规范](https://datatracker.ietf.org/doc/html/rfc7208).
|
||||
|
||||
### 配置 DKIM
|
||||
|
||||
DKIM (DomainKeys Identified Mail) 用于验证邮件的完整性和真实性.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> 对于使用 SMTP 中继服务的场景, DKIM 通常由服务商负责配置和签署, 本地 DKIM 在中继场景下不一定会被最终保留或使用, 其价值更多在于内部一致性或未来切换为直连发信的准备.
|
||||
|
||||
> 如果选择直接发信而非使用 SMTP 中继服务, 则本节内容是**必须的**.
|
||||
|
||||
1. 生成 DKIM 密钥:
|
||||
|
||||
```bash
|
||||
docker exec -it mailserver setup config dkim <选择器名称>
|
||||
```
|
||||
|
||||
这会输出公钥, 记下来.
|
||||
|
||||
`<选择器名称>` 可以省略, 默认为 `mail`. 选择器名称用于区分同一域名下的不同 DKIM 密钥.
|
||||
|
||||
如果使用的不是 `rspamd` 而是 `opendkim`, 过程会略有不同. 此时会输出一个容器内路径, 需要到本地的 `./config/opendkim/keys/` 目录下找到对应的公钥文件.
|
||||
|
||||
2. 添加 DKIM 记录 (TXT 记录):
|
||||
- 主机名: `mail._domainkey`
|
||||
- 值: 上一步获取的公钥内容. 大概是这样的:
|
||||
|
||||
```plain
|
||||
v=DKIM1; k=rsa; p=MIISncASjsASK...
|
||||
```
|
||||
|
||||
解释:
|
||||
- `mail`: 选择的选择器名称, 与生成密钥时使用的选择器一致.
|
||||
- `_domainkey`: 固定值, 指示这是一个 DKIM 记录.
|
||||
|
||||
### 配置 DMARC
|
||||
|
||||
DMARC (Domain-based Message Authentication, Reporting, and Conformance) 用于指定邮件的处理策略. 更多有关 DMARC Alignment 的内容参见 [后续章节](#dmarc-alignment).
|
||||
|
||||
在 DNS 服务商处添加以下记录:
|
||||
|
||||
- DMARC 记录 (TXT 记录):
|
||||
- 主机名: `_dmarc`
|
||||
- `v=DMARC1; p=none; sp=none; rua=mailto:me@domain.tld`
|
||||
|
||||
解释:
|
||||
- `v=DMARC1`: 指定 DMARC 版本.
|
||||
- `p=none`: 对未通过 DMARC 检查的邮件不采取任何措施.
|
||||
- `sp=none`: 对子域名的策略同样为 none.
|
||||
- `rua=mailto`: 如果希望收到报告, 可以指定一个邮箱地址.
|
||||
|
||||
在运行一段时间并查看报告无误后, 可以将 `p` 和 `sp` 设置为 `quarantine` 或 `reject`, 以增强防护. `quarantine` 表示将可疑邮件标记为垃圾邮件, `reject` 则表示直接拒绝这些邮件.
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 同一个域名下只能有一个 DMARC 记录.
|
||||
|
||||
### 启动!
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
看一眼 log, 没问题的话就可以下一步了.
|
||||
|
||||
### Rspamd Web UI
|
||||
|
||||
1. 设置密码
|
||||
|
||||
```bash
|
||||
docker exec -it mailserver rspamadm pw -p <密码>
|
||||
```
|
||||
|
||||
会输出以 `$2$` 开头的很长的字符串, 记下来.
|
||||
|
||||
2. 创建配置文件
|
||||
|
||||
```bash
|
||||
sudo mkdir -p config/rspamd/override.d
|
||||
sudoedit config/rspamd/override.d/worker-controller.inc
|
||||
```
|
||||
|
||||
写入 **(注意分号)**:
|
||||
|
||||
```plain
|
||||
# 前面得到的很长一串
|
||||
password = "$2$...";
|
||||
```
|
||||
|
||||
3. 重启容器
|
||||
|
||||
```bash
|
||||
docker compose restart mailserver
|
||||
```
|
||||
|
||||
4. 访问
|
||||
|
||||
然后就和其他 WebUI 一样了. 可以暴露 11334 端口然后通过 http 访问, 也可以通过 SSH 隧道本地访问, 也可以反向代理, 等等等等, 怎样都好.
|
||||
|
||||
## 配置邮件客户端
|
||||
|
||||
我并非 TUI 重度爱好者, 日常用 Thunderbird 当客户端, 因此这里只涉及这一种客户端的配置方法, 当然其他的也大同小异.
|
||||
|
||||
### Thunderbird
|
||||
|
||||
1. 在添加邮箱的第一个页面, 点击 `MANUAL CONFIGURATION`.
|
||||
|
||||
2. Incoming server settings:
|
||||
- Protocol: IMAP
|
||||
- Server hostname: `mail.domain.tld`
|
||||
- Port: `993`
|
||||
- SSL: `SSL/TLS`
|
||||
- Authentication: `Normal password`
|
||||
|
||||
3. Outgoing server settings:
|
||||
- Server hostname: `mail.domain.tld`
|
||||
- Port: `587`
|
||||
- SSL: `STARTTLS`
|
||||
- Authentication: `Normal password`
|
||||
|
||||
或使用安全性更高的 SMTPS:
|
||||
- Server hostname: `mail.domain.tld`
|
||||
- Port: `465`
|
||||
- SSL: `SSL/TLS`
|
||||
- Authentication: `Normal password`
|
||||
|
||||
> 为什么是英语? 因为我的 LANG 是 en_US.UTF-8 :)
|
||||
|
||||
4. 点击 Test 按钮, 如果一切正常, 会显示成功信息.
|
||||
|
||||
5. 输入密码, 完成配置.
|
||||
|
||||
现在已经可以试着和其他邮箱互发邮件了!
|
||||
|
||||
## Extra Notes
|
||||
|
||||
本节包含一些额外的说明和可选配置.
|
||||
|
||||
### exim4: 我呢?
|
||||
|
||||
这个, 不需要了. 既然已经有了邮箱服务, 那么继续使用重量级的 `exim4` 就没什么意义了.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> 如果不需要系统内部邮件发送功能, 可以放心大胆地跳过本节剩余内容.
|
||||
|
||||
1. 可以安装 `ssmtp` 或 `msmtp` 之类的轻量级 MTA, 用于系统内部邮件发送. 这里用 `msmtp` 作为示例:
|
||||
|
||||
```bash
|
||||
sudo apt install msmtp msmtp-mta bsd-mailx
|
||||
```
|
||||
|
||||
这个过程会自动卸载 `exim4`.
|
||||
|
||||
2. 创建用于内网发信的邮箱地址:
|
||||
|
||||
```bash
|
||||
docker exec -it mailserver setup email add notification@domain.tld <密码>
|
||||
```
|
||||
|
||||
3. 然后创建配置文件 `/etc/msmtprc`:
|
||||
|
||||
```plain
|
||||
defaults
|
||||
auth on
|
||||
tls on
|
||||
tls_starttls off
|
||||
tls_trust_file /etc/ssl/certs/ca-certificates.crt
|
||||
logfile /var/log/msmtp.log
|
||||
|
||||
account system-notifier
|
||||
host mail.domain.tld
|
||||
port 465
|
||||
from notifier@domain.tld
|
||||
user notifier@domain.tld
|
||||
password <密码>
|
||||
|
||||
account default : system-notifier
|
||||
|
||||
aliases /etc/aliases
|
||||
```
|
||||
|
||||
可替换的部分:
|
||||
- `system-notifier`: 账户名称, 随便取.
|
||||
|
||||
- `mail.domain.tld`: 邮件服务器地址.
|
||||
|
||||
- `465`: 端口, 也可以使用 `587` 并将 `tls_starttls` 设置为 `on`. 前者使用更安全的 SMTPS, 后者使用 STARTTLS.
|
||||
|
||||
- `notifier@domain.tld`: 用于发信的邮箱地址, 这里使用刚才创建的地址.
|
||||
|
||||
- `<密码>`: 刚才创建地址时设置的密码.
|
||||
|
||||
- `/var/log/msmtp.log`: 日志文件路径, 注意文件权限.
|
||||
|
||||
上述配置使用了 TLS 加密和真实的邮箱与账号. 当然可以通过其他方式绕过限制从而使用任意并不需要真实存在的邮箱地址发信, 但既然存在更安全的实践, 何乐而不为呢.
|
||||
|
||||
4. 编辑 `/etc/aliases`, 添加如下内容:
|
||||
|
||||
```plain
|
||||
root: me@domain.tld
|
||||
default: me@domain.tld
|
||||
```
|
||||
|
||||
将 `me@domain.tld` 替换为希望用于接收系统邮件的邮箱地址.
|
||||
|
||||
5. 测试:
|
||||
|
||||
```bash
|
||||
echo "This is a test email from the system." | mail -s "Test Email" root
|
||||
```
|
||||
|
||||
如果一切正常, 应该会在前面配置的邮箱客户端中收到这封测试邮件.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> 此时可将 msmtp 看作 mailserver 的客户端, 因此 `/etc/msmtprc` 中配置的邮件服务器并不一定要部署在本机, 甚至可以是公共邮箱服务.
|
||||
|
||||
### Catch'em All!
|
||||
|
||||
如果希望接收发往不存在邮箱地址的邮件, 可以启用 Catch-all 功能. 方法也很简单, 使用 alias 即可:
|
||||
|
||||
```bash
|
||||
docker exec -it mailserver setup alias add @domain.tld me@domain.tld
|
||||
```
|
||||
|
||||
将其中 `me@domain.tld` 替换为希望接收这些邮件的真实邮箱地址即可.
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> 完全按照上述步骤配置通配符邮箱别名可能会导致后续添加其他邮箱时邮件仍然发到上面指定的 catch-all 邮箱而不是新创建的邮箱. 更好的实践是用到什么邮箱名再创建对应的别名.
|
||||
|
||||
### MTA-STS
|
||||
|
||||
**MTA-STS** (Mail Transfer Agent Strict Transport Security) 通过强制要求发送方使用加密连接发送邮件防止中间人攻击, 对个人邮箱来讲~~看起来其实没啥大用但总归~~是个加分项, 并且确实会让邮箱服务变得更酷.
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 使用 SMTP 中继服务发送邮件时, MTA-STS 并不会生效, 因为发送方并非直接连接到自己的邮件服务器. 但是对于入站邮件来说, MTA-STS 依然有效.
|
||||
|
||||
1. 前置要求
|
||||
|
||||
除了 [开头](#需要什么) 中提到的要求外, 还需要:
|
||||
- 部署 HTTPS 静态网站的能力, 用于托管 MTA-STS 策略文件, 且该文件必须通过 HTTPS 提供.
|
||||
|
||||
- 将 `mta-sts.domain.tld` 指向该静态网站的能力, 且访问时返回的 SSL 证书中的 CN 或 SAN 必须包含 `mta-sts.domain.tld`.
|
||||
|
||||
2. 新增 DNS 记录
|
||||
- A 记录:
|
||||
- 主机名: `mta-sts`
|
||||
- 值: 指向托管 MTA-STS 策略文件的服务器 IP 地址.
|
||||
|
||||
如果不在自己的服务器上托管, 可以使用 `CNAME` 记录指向第三方提供的静态网站托管服务.
|
||||
|
||||
- MTA-STS 发现记录 (TXT 记录):
|
||||
- 主机名: `_mta-sts`
|
||||
- 值: `v=STSv1; id=2026010101`
|
||||
|
||||
解释:
|
||||
- `v=STSv1`: 指定 MTA-STS 版本.
|
||||
- `id=2026010101`: 策略文件的版本号, 每次更新策略文件时需要更改此值以通知发送方.
|
||||
|
||||
- TLS-RPT 报告记录 (TXT 记录):
|
||||
|
||||
指定接收 TLS 报告的邮箱地址.
|
||||
- 主机名: `_smtp._tls`
|
||||
- 值: `v=TLSRPTv1; rua=mailto:me@domain.tld`
|
||||
|
||||
解释:
|
||||
- `v=TLSRPTv1`: 指定 TLS-RPT 版本.
|
||||
- `rua=mailto:me@domain.tld`: 指定接收报告的邮箱地址.
|
||||
|
||||
3. 创建静态网站
|
||||
|
||||
不展开了, 使用 OpenResty 或者直接用 Nginx 甚至借助 Github pages / Netlify / Cloudflare pages 等等都可以, 总之满足前面提到的要求就行.
|
||||
|
||||
4. 创建 MTA-STS 策略文件
|
||||
|
||||
在 `mta-sts.domain.tld` 网站根目录下创建 `.well-known/mta-sts.txt` 文件, 内容如下:
|
||||
|
||||
```plain
|
||||
version: STSv1
|
||||
mode: testing
|
||||
mx: mail.domain.tld
|
||||
max_age: 86400
|
||||
```
|
||||
|
||||
解释:
|
||||
- `version: STSv1`: 指定 MTA-STS 版本.
|
||||
|
||||
- `mode: testing`: 策略模式, 可选值有 `enforce`, `testing`, `none`. `enforce` 表示强制执行策略, `testing` 表示仅测试不强制执行, `none` 表示不启用 MTA-STS. 初始阶段建议使用 `testing`, 确认无误后再改为 `enforce`.
|
||||
|
||||
- `mx: mail.domain.tld`: 指定允许发送邮件的 MX 服务器.
|
||||
|
||||
- `max_age: 86400`: 策略的最大缓存时间, 单位为秒. 这里设置为 86400 秒(1 天). 建议在测试结束确认无误后将此值调大一些, 例如一周(604800 秒) 或更长.
|
||||
|
||||
5. 验证配置
|
||||
- 通过 [Hardenize](https://www.hardenize.com/) 或类似的在线工具验证 MTA-STS 配置是否正确.
|
||||
|
||||
- 从支持 MTA-STS 的邮箱服务商 (例如 Gmail) 发送测试邮件, 查看 Postfix 日志来验证入站时 TLS 握手等流程是否符合预期. 至于 MTA-STS 是否真的生效, 可以稍后查看 TLS-RPT 报告邮箱收到的相关报告来确认.
|
||||
|
||||
### DMARC Alignment
|
||||
|
||||
这是一种收信方采取的安全策略, 用于确保发件人的身份与邮件头部的 `From` 地址一致.
|
||||
|
||||
- 两种模式
|
||||
1. Relaxed (默认)
|
||||
|
||||
只要域名相同或子域名关系即可通过.
|
||||
|
||||
2. Strict
|
||||
|
||||
必须完全相同才可通过.
|
||||
|
||||
可以在 DMARC 记录中通过 `adkim` 和 `aspf` 标签指定 DKIM 和 SPF 的 Alignment 模式, 例如:
|
||||
|
||||
```plain
|
||||
v=DMARC1; p=none; adkim=r; aspf=r;
|
||||
```
|
||||
|
||||
指定了 DKIM 和 SPF 都使用 Relaxed 模式.
|
||||
|
||||
使用 SMTP 中继服务时, **必须**使用 Relaxed 模式, 原因后续会说明.
|
||||
|
||||
- 两个步骤
|
||||
1. SPF Alignment
|
||||
|
||||
匹配 `Return-Path` 地址的域名和 `From` 头部的域名是否相同或子域名关系.
|
||||
|
||||
2. DKIM Alignment
|
||||
|
||||
匹配 DKIM 签名中 `d=` 标签的域名和 `From` 头部的域名是否相同或子域名关系.
|
||||
|
||||
二者只要有一个通过即可满足 DMARC Alignment 要求.
|
||||
|
||||
- SMTP 中继发信时如何工作
|
||||
- 对于 SPF Alignment
|
||||
|
||||
以 Resend 为例, 其发出的邮件中 `Return-Path` 地址虽然会被改写, 但会被改写为 `<很长一串>@send.domain.tld`, 其域名属于 `domain.tld` 的子域名, 因此在 Relaxed 模式下可以通过 SPF Alignment 检查.
|
||||
|
||||
- 对于 DKIM Alignment
|
||||
|
||||
以 Resend 为例, 在其让添加进 DNS 的记录中有一条便是选择器为 `resend` 的 DKIM 记录, resend 会使用该密钥对发出的邮件进行签名, 签名中的 `d=` 标签会被设置为 `domain.tld`, 因此可以通过 DKIM Alignment 检查.
|
||||
|
||||
### 转发到其他域
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> 大多数 SMTP 中继服务采用严格的发件人核验制度, 即发件人的域名必须是经过验证和配置的域名. 因此如果使用类似的服务发信, 那么自动转发到其他域名很有可能是不可能的事情.
|
||||
>
|
||||
> 因此本节内容更适合自建邮件服务器并直接发信的场景, 对于没有强商业目的的个人邮箱来说意义不大.
|
||||
|
||||
- SRS (Sender Rewriting Scheme)
|
||||
|
||||
用于在转发邮件时重写发件人地址, 改写 envelope sender 为自己域名下的地址, 从而避免 SPF 检查失败的问题.
|
||||
|
||||
对于 docker-mailserver, 只需要在 `compose.yaml` 中添加 `ENABLE_SRS=1` 环境变量即可. 其他相关配置参见 [官方文档](https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/#srs-sender-rewriting-scheme).
|
||||
|
||||
> 在此处我遇到了一个极其诡异的情况, 简单提一嘴:
|
||||
>
|
||||
> - What
|
||||
>
|
||||
> 启用 SRS 后, postfix 迟迟无法启动, postsrsd 长时间以 CPU 满负荷持续运行.
|
||||
>
|
||||
> - Why
|
||||
>
|
||||
> 如果使用 strace 跟踪 postsrsd 进程, 会发现它在不停地尝试关闭一些文件描述符, 类似这样:
|
||||
>
|
||||
> ```plain
|
||||
> close(192479922) = -1 EBADF (Bad file descriptor)
|
||||
> ```
|
||||
>
|
||||
> 原因是 postsrsd 在启动时,为了确保环境干净,会执行"关闭从 3 到 MAX 的所有文件描述符"的操作, 而 MAX 的值通过 `sysconf(_SC_OPEN_MAX)` 或 `getrlimit(RLIMIT_NOFILE)` 获取. 在 Docker 容器中, 这个值可能会非常大, 导致 postsrsd 花费大量时间尝试关闭这些不存在的文件描述符.
|
||||
>
|
||||
> - How
|
||||
>
|
||||
> 通过在 `compose.yaml` 中添加 `ulimits` 配置, 将 `nofile` 的软限制和硬限制都设置为一个合理的值, 例如 65535:
|
||||
>
|
||||
> ```yaml
|
||||
> services:
|
||||
> mailserver:
|
||||
> ...
|
||||
> ulimits:
|
||||
> nofile:
|
||||
> soft: 65535
|
||||
> hard: 65535
|
||||
> ```
|
||||
>
|
||||
> 之后重建容器即可.
|
||||
|
||||
- ARC (Authenticated Received Chain)
|
||||
|
||||
用于在邮件转发过程中保留并传递上游服务器对 SPF / DKIM / DMARC 的认证结果, 从而帮助下游服务器在 DMARC 检查失败时判断邮件是否原本是可信的.
|
||||
- 为什么需要 ARC
|
||||
|
||||
SRS 会改写 Return-Path, 在 From 域不属于转发域的情况下这会破坏 SPF 对齐. 如果同时一些受 DKIM 签名中 `h=` 标签保护的 Header (在不知情的情况下)调整顺序/增/删/改, 或邮件内容改变, 则会导致 DKIM 签名失效, 从而导致 DMARC 检查失败. 此时如果启用了 ARC, 则可以通过 ARC 链来证明邮件的真实性.
|
||||
|
||||
但同时, ARC 工作的前提条件是转发服务器受目标服务器信任. 这又是一个黑盒, 涉及漫长和玄学的信誉培养过程.
|
||||
|
||||
- 如何启用
|
||||
|
||||
对于启用 Rspamd 的 docker-mailserver, 需要在 `config/rspamd/override.d/arc.conf` 中添加如下内容:
|
||||
|
||||
```plain
|
||||
sign_local = true;
|
||||
sign_authenticated = true;
|
||||
|
||||
allow_env_sender_mismatch = true;
|
||||
allow_hdr_from_mismatch = true;
|
||||
|
||||
domain {
|
||||
domain.tld {
|
||||
path = "/path/to/dkim/private/key.private.txt";
|
||||
selector = "mail";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
替换 `domain.tld` 和 `path` 以及 `selector` 为真实值. Rspamd 中, ARC 通常使用和 DKIM 相同的密钥对邮件进行签名.
|
||||
|
||||
`allow_env_sender_mismatch` 和 `allow_hdr_from_mismatch` 用于在转发导致的发件人域名与当前服务器域名不一致时仍然进行签名.
|
||||
|
||||
详细配置参见 [Rspamd 文档](https://docs.rspamd.com/modules/arc/).
|
||||
|
||||
### 查看报告
|
||||
|
||||
上述配置中的 DMARC 和 TLS-RPT 都会发送报告到指定邮箱. 可以定期查看这些报告以了解邮件传输的安全状况和潜在问题, 推荐部署 [Parsedmarc](https://github.com/domainaware/parsedmarc) 或类似工具进行自动化处理.
|
||||
|
||||
### 邮件传输链路
|
||||
|
||||
> 一些关于服务器位置描述的解释:
|
||||
>
|
||||
> - `Local`: 自建邮件服务器.
|
||||
> - `Source`: 外部邮件服务器, 发送方.
|
||||
> - `Destination`: 外部邮件服务器, 接收方.
|
||||
> - `Relay`: SMTP 中继服务.
|
||||
|
||||
以本文配置为例:
|
||||
|
||||
- 收信
|
||||
1. 外部投递 (Source MTA to Local MTA):
|
||||
- 连接方: 外部邮件服务器.
|
||||
|
||||
- 被连接方: 自建邮件服务器公网 IP.
|
||||
|
||||
- 目标端口: 25.
|
||||
|
||||
- 说明: 标准的 SMTP 传输. 这也是为什么需要开放 25 端口入站.
|
||||
|
||||
2. 本地分发 (Local MTA to Local MDA):
|
||||
- 连接方: 自建邮件服务器.
|
||||
|
||||
- 被连接方: 自建邮件服务器本地存储.
|
||||
|
||||
- 端口: 本地通信, 无需端口.
|
||||
|
||||
- 说明: Postfix (MTA) 接收邮件后,转交给 Dovecot (MDA) 将邮件存入磁盘.
|
||||
|
||||
3. 客户端拉取 (MUA to Local MDA):
|
||||
- 连接方: 邮件客户端 (例如 Thunderbird).
|
||||
|
||||
- 被连接方: 自建邮件服务器公网 IP.
|
||||
|
||||
- 目标端口: 993 (IMAPS).
|
||||
|
||||
- 说明: 使用 IMAPS 拉取邮件.
|
||||
|
||||
- 发信
|
||||
1. 客户端提交 (MUA to Local MSA):
|
||||
- 连接方: 邮件客户端 (例如 Thunderbird).
|
||||
|
||||
- 被连接方: 自建邮件服务器公网 IP.
|
||||
|
||||
- 目标端口: 587 (STARTTLS) 或 465 (SSL/TLS).
|
||||
|
||||
- 说明: 把邮件上传到服务器队列中.
|
||||
|
||||
2. 中继转发 (Local MTA to Relay MTA):
|
||||
- 连接方: 自建邮件服务器.
|
||||
|
||||
- 被连接方: SMTP 中继服务.
|
||||
|
||||
- 目标端口: 587 (STARTTLS).
|
||||
|
||||
- 说明: 自建邮件服务器根据 `DEFAULT_RELAY_HOST` 配置, 验证账号密码后将邮件转交给中继商.
|
||||
|
||||
3. 最终投递 (Relay MTA to Destination MTA):
|
||||
- 连接方: SMTP 中继服务.
|
||||
|
||||
- 被连接方: 外部邮件服务器.
|
||||
|
||||
- 目标端口: 25.
|
||||
|
||||
- 说明: 这一步由中继商完成. 部分客户端可能会显示代发信息, 具体取决于中继实现与客户端策略.
|
||||
|
||||
### 备份与恢复
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> 邮件数据一旦丢失几乎不可恢复, 务必定期离机备份.
|
||||
|
||||
本文采用 docker 部署邮件服务器, 容器本身是无状态的, 因此只需要备份挂载的卷即可, 不做过多赘述.
|
||||
|
||||
唯一需要特殊说明的是务必保留文件权限和属主信息, 否则恢复后邮件服务器可能无法正常工作.
|
||||
|
||||
### 测试工具
|
||||
|
||||
一些上文中提到或者没提到的在线测试工具:
|
||||
|
||||
- [Mail-Tester](https://www.mail-tester.com): 用于测试邮件是否容易被判定为垃圾邮件.
|
||||
|
||||
- [MailGenius](https://www.mailgenius.com/): 比 Mail-Tester 更全面和严格的测试工具.
|
||||
|
||||
- [Hardenize](https://www.hardenize.com/): 用于测试域名的安全配置, 其中也包括 DMARC, MTA-STS 等邮件服务器相关的配置.
|
||||
|
||||
- [MXToolbox](https://mxtoolbox.com/): 提供多种邮件相关的测试工具, 包括黑名单检查, SMTP 测试等等.
|
||||
|
||||
- [GMass](https://www.gmass.co/inbox): 提供了 15 个 Gmail 邮箱用于测试邮件送达率.
|
||||
|
||||
- [Google Postmaster Tools](https://postmaster.google.com/v2/sender_compliance): 如果主要收件方是 Gmail 且选择自己发信而不使用 SMTP 中继服务, 可以注册并使用该工具来监控邮件送达情况和声誉. 需要注意的是即使"Compliance status"中全部项为绿色, 也不代表邮件一定不会被扔进垃圾箱, 仅供参考.
|
||||
|
||||
---
|
||||
|
||||
> What, Why, How. 以上是 What 和 How, 接下来是...
|
||||
|
||||
## 这一切到底是为什么?
|
||||
|
||||
排除利益驱动, 我能想到的最合适的借口就是"隐私". 可是如果登陆 Resend 的后台看一眼, 就会发现我写的邮件被一封封明晃晃地不加掩饰地放在那里, 所有的内容都以明文的方式被看得一清二楚. 此时和使用公共邮箱服务唯一的区别似乎也就只剩"有一个很酷的后缀"这一点了. 即便真的解决了 25 端口问题 (是的, 写完上述内容的几天后我确实做到了) 从而得以摆脱 SMTP 中继服务, 在惊讶于明明 Mail-Tester 给出了 10/10 的满分评价但还是被 Gmail 扔进垃圾箱的残酷现实之余, 我也意识到自建邮局这条路仅靠热情是绝对走不通的. 一是预热 IP 需要花费的时间成本乃至金钱成本远超我的想象, 二是无论如何邮件内容也会被收件方的平台以算法评估一遍的事实彻底击碎了对于"隐私"乌托邦最后的妄想. 即使有这么多复杂的安全措施, 补齐了传输过程中的每一个可能的安全漏洞, 但真正和我点对点沟通的从来都只是靠着一条条既定规则维系的平台, 而不是我在写下收件地址时心中所想的一个个鲜活的人.
|
||||
|
||||
那么做这一切的目的到底是什么呢? 或许也只剩"酷"了吧, 除此之外我唯一能想到的好处就是在仅需要邮箱就能注册账号的平台注册无穷无尽的小号.
|
||||
|
||||
Anyway, 确实是我拥有域名和服务器以来部署过的最复杂, 折腾时间最久, 也可能是最无用的服务. 仅论过程还是很有意思的, 至少能让我理解现代电子邮箱系统到底是如何运作的. ~~闲的没事的话~~还是很值得试一试的 :)
|
||||
|
||||
EOF
|
||||
@@ -1,3 +1,5 @@
|
||||
de 布局太全能了
|
||||
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/3/3e/KB_Germany_Linux.svg" alt="Deutsche Tastaturbelegung unter Linux"/>
|
||||
|
||||
```
|
||||
|
||||
+3
-3
@@ -6,11 +6,11 @@
|
||||
>
|
||||
> sddm: `cachyos-v3/sddm 0.21.0-8`
|
||||
|
||||
SDDM shows a blank screen (only with mouse cursor and tty cursor) after booting into graphical target, but starts normally after restarting the SDDM service.
|
||||
SDDM shows a blank screen (possibly only with mouse cursor and tty cursor) after booting into graphical target, but starts normally after restarting the SDDM service.
|
||||
|
||||
## Why
|
||||
|
||||
SDDM starts before the NVIDIA driver is fully initialized, causing the greeter session to fail to display properly.
|
||||
SDDM started before the NVIDIA driver is fully initialized, causing the greeter session to fail to display anything.
|
||||
|
||||
<details>
|
||||
<summary>evidence</summary>
|
||||
@@ -114,7 +114,7 @@ sddm[1150]: Greeter session started successfully
|
||||
|
||||
## How
|
||||
|
||||
The key is to avoid timing race between NVIDIA initialization and SDDM start. There are multiple ways to achieve this, two for example:
|
||||
The key is to avoid timing race between NVIDIA initialization and SDDM startup. There are multiple ways to achieve this, here are two:
|
||||
|
||||
### Early KMS:
|
||||
|
||||
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
> some sample scripts and personal notes regarding VVenC usage.
|
||||
>
|
||||
> Scripts below use `vvencapp` for better control over encoding parameters. However, `libvvenc` in ffmpeg can be used as well and is generally recommonded ~~for better quality of life~~.
|
||||
|
||||
## VBR with 2 passes
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# path = directory of this script
|
||||
path="$(dirname "$0")"
|
||||
# name = identifier of the script and its outputs
|
||||
name="$(basename "$0" .sh)"
|
||||
# io paths
|
||||
input="$1"
|
||||
output="${path}/${name}.266"
|
||||
# slice options
|
||||
slice_start=510
|
||||
slice_duration=30
|
||||
slice_arg=()
|
||||
#slice_arg=(-ss "$slice_start" -t "$slice_duration")
|
||||
# video properties
|
||||
size=1920x1080
|
||||
frame_rate="24000/1001" # 23.976
|
||||
pix_fmt="yuv420p"
|
||||
# VMAF threads (should leave some for 266 decoding)
|
||||
vmaf_threads=$(($(nproc) / 2))
|
||||
# mailto address, leave empty to use desktop notifications instead.
|
||||
mailto=""
|
||||
# encoder params
|
||||
params=(
|
||||
--y4m 1
|
||||
--preset fast -b 3000k --passes 2 --rcstatsfile stats.json -rs 4
|
||||
--profile main_10 --tier high --threads 2 --qpa 0
|
||||
)
|
||||
|
||||
mail_error() {
|
||||
local subject="$name: Error occurred"
|
||||
local body="An error occurred on line $1 while executing the script $0."
|
||||
if [ -n "$mailto" ]; then
|
||||
echo "$body" | mail -s "$subject" "$mailto"
|
||||
else
|
||||
notify-send "$subject" "$body"
|
||||
fi
|
||||
}
|
||||
|
||||
mail_success() {
|
||||
local subject="$name: Completed successfully"
|
||||
local body
|
||||
body=$(printf "VMAF: %s\nSize in bytes: %s\n" \
|
||||
"$(jq '.pooled_metrics.vmaf' "${path}/${name}.vmaf.json")" \
|
||||
"$(stat -c%s "$output")")
|
||||
if [ -n "$mailto" ]; then
|
||||
echo "$body" | mail -s "$subject" "$mailto"
|
||||
else
|
||||
notify-send "$subject" "$body"
|
||||
fi
|
||||
}
|
||||
|
||||
trap 'mail_error $LINENO' ERR
|
||||
|
||||
decode() {
|
||||
ffmpeg -hide_banner -nostdin \
|
||||
-hwaccel auto -r "$frame_rate" "${slice_arg[@]}" -i "$1" \
|
||||
-pix_fmt "$pix_fmt" -f yuv4mpegpipe - 2>/dev/null
|
||||
}
|
||||
|
||||
pass1() {
|
||||
vvencapp -i "$1" "${params[@]}" --pass 1 -o /dev/null
|
||||
}
|
||||
|
||||
pass2() {
|
||||
vvencapp -i "$1" "${params[@]}" --pass 2 -o "$2"
|
||||
}
|
||||
|
||||
calc_vmaf() {
|
||||
ffmpeg -hide_banner -nostdin \
|
||||
-hwaccel auto -r "$frame_rate" "${slice_arg[@]}" -i "$1" \
|
||||
-r "$frame_rate" -i "$2" \
|
||||
-filter_complex "[0:v][1:v]libvmaf=log_fmt=json:log_path=${path}/${name}.vmaf.json:n_threads=$vmaf_threads" \
|
||||
-f null -
|
||||
}
|
||||
|
||||
decode "$input" | pass1 "-"
|
||||
decode "$input" | pass2 "-" "$output"
|
||||
calc_vmaf "$input" "$output"
|
||||
|
||||
mail_success
|
||||
```
|
||||
|
||||
- script explained:
|
||||
- `input="$1"`: The source video file is passed as the first argument to the script.
|
||||
|
||||
- `slice_start` and `slice_duration`: Define the start time and duration of the video slice to be processed. Uncomment the `slice_arg` line to enable slicing.
|
||||
|
||||
- `mailto=""` requires capability to send emails from command line, i.e. a **MTA** (Mail Transfer Agent) must be installed and configured in the system. Leave empty to use notify-send to send desktop notifications instead.
|
||||
|
||||
- encoder parameters explained:
|
||||
- `-b 3000k`: Target bitrate. The output bitrate could be significantly different (commonly 1/2 to 1/3 lower than target).
|
||||
|
||||
- `-rs 4` or `--refreshsec 4`: Intra period/refresh in seconds. Higher for better compression, lower for better seeking.
|
||||
|
||||
- `--profile main_10`: 10-bit Main profile. Change to `main` for 8-bit encoding.
|
||||
|
||||
- `--tier high`: High tier for better quality at higher bitrates.
|
||||
|
||||
- `--threads 2`: Generally fewer threads yield better encoding quality yet slower speed. The maximum number of parallel frames is determined automatically according to frame size, and might be lower than the thread count specified here.
|
||||
|
||||
- `--qpa 0`: Disable "perceptually motivated QP adaptation", do so if you care about quality metrics or compress with archival purposes.
|
||||
|
||||
- `-q` or `--qp` will be ignored in VBR mode even if specified.
|
||||
|
||||
## CQP
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# path = directory of this script
|
||||
path="$(dirname "$0")"
|
||||
# name = identifier of the script and its outputs
|
||||
name="$(basename "$0" .sh)"
|
||||
# io paths
|
||||
input="$1"
|
||||
output="${path}/${name}.266"
|
||||
# slice options
|
||||
slice_start=510
|
||||
#slice_arg=(-ss "$slice_start" -t "$slice_duration")
|
||||
slice_duration=30
|
||||
slice_arg=()
|
||||
# video properties
|
||||
size=1920x1080
|
||||
frame_rate="24000/1001" # 23.976
|
||||
pix_fmt="yuv420p"
|
||||
# VMAF threads (should leave some for 266 decoding)
|
||||
vmaf_threads=$(($(nproc) / 2))
|
||||
# mailto address
|
||||
mailto=""
|
||||
# encoder params
|
||||
params=(
|
||||
--y4m 1
|
||||
--preset fast -q 16 -rs 4
|
||||
--profile main_10 --tier high --qpa 0
|
||||
)
|
||||
|
||||
mail_error() {
|
||||
local subject="$name: Error occurred"
|
||||
local body="An error occurred on line $1 while executing the script $0."
|
||||
if [ -n "$mailto" ]; then
|
||||
echo "$body" | mail -s "$subject" "$mailto"
|
||||
else
|
||||
notify-send "$subject" "$body"
|
||||
fi
|
||||
}
|
||||
|
||||
mail_success() {
|
||||
local subject="$name: Completed successfully"
|
||||
local body
|
||||
body=$(printf "VMAF: %s\nSize in bytes: %s\n" \
|
||||
"$(jq '.pooled_metrics.vmaf' "${path}/${name}.vmaf.json")" \
|
||||
"$(stat -c%s "$output")")
|
||||
if [ -n "$mailto" ]; then
|
||||
echo "$body" | mail -s "$subject" "$mailto"
|
||||
else
|
||||
notify-send "$subject" "$body"
|
||||
fi
|
||||
}
|
||||
|
||||
trap 'mail_error $LINENO' ERR
|
||||
|
||||
decode() {
|
||||
ffmpeg -hide_banner -nostdin \
|
||||
-hwaccel auto -r "$frame_rate" "${slice_arg[@]}" -i "$1" \
|
||||
-pix_fmt "$pix_fmt" -f yuv4mpegpipe - 2>/dev/null
|
||||
}
|
||||
|
||||
encode() {
|
||||
vvencapp -i "$1" "${params[@]}" -o "$2"
|
||||
}
|
||||
|
||||
calc_vmaf() {
|
||||
ffmpeg -hide_banner -nostdin \
|
||||
-hwaccel auto -r "$frame_rate" "${slice_arg[@]}" -i "$1" \
|
||||
-r "$frame_rate" -i "$2" \
|
||||
-filter_complex "[0:v][1:v]libvmaf=log_fmt=json:log_path=${path}/${name}.vmaf.json:n_threads=$vmaf_threads" \
|
||||
-f null -
|
||||
}
|
||||
|
||||
decode "$input" | encode "-" "$output"
|
||||
calc_vmaf "$input" "$output"
|
||||
|
||||
mail_success
|
||||
```
|
||||
|
||||
- script explained:
|
||||
|
||||
same as above.
|
||||
|
||||
- encoder parameters explained:
|
||||
- `-q 16`: Quantization Parameter, lower qp means better quality, larger file size and slower encoding speed.
|
||||
|
||||
- `-rs 4 --profile main_10 --tier high --qpa 0`: Same as above.
|
||||
|
||||
- `--fga 0`: Disable "film grain analysis" (default disabled). fga in current version of VVenC (1.13.1) is kinda buggy and may lead to crashes in CQP mode. Enable with caution.
|
||||
|
||||
- `--maxrate` or `-m` can be used to set a maximum bitrate. However, `--qpa 1` is required for this to take effect. With these two parameters enabled, this certain pattern of rc theoretically becomes **CQF** (Constant Quality Factor) and behaves noticeably differently comparing to CQP.
|
||||
|
||||
## Extra notes
|
||||
|
||||
- Remux 266 bare stream into MKV:
|
||||
|
||||
```bash
|
||||
ffmpeg -i input.266 -c:v copy -bsf:v setts=ts='N*(1001/24000)/TB' output.mkv
|
||||
```
|
||||
|
||||
The `bsf:v setts` part is necessary to set correct timestamps, replace `1001/24000` with the reciprocal of the actual frame rate.
|
||||
|
||||
## References
|
||||
|
||||
- [Usage - fraunhoferhhi/vvenc Wiki](https://github.com/fraunhoferhhi/vvenc/wiki/Usage)
|
||||
|
||||
- [setts - FFmpeg Bitstream Filters Documentation](https://ffmpeg.org/ffmpeg-bitstream-filters.html#setts)
|
||||
Reference in New Issue
Block a user