2 Commits

Author SHA1 Message Date
69157c90cb Merge pull request 'v0.2.0' (#2) from develop into main
Reviewed-on: #2
2025-11-29 16:12:03 +05:00
e76d2ae398 Merge pull request 'v0.1.0' (#1) from develop into main
Reviewed-on: #1
2025-11-08 17:34:06 +05:00
15 changed files with 14 additions and 676 deletions

View File

@@ -1,16 +1,3 @@
## 0.3.0 (soon)
***
#### Русский
* Служба systemd
* Изменено WantedBy с multi-user.target на multi-user.target
* Убрано ExecStop. По факту это не работало. Чтобы остановить сервис с очисткой правил nftables выпоните команду: kor-elf-shield stop
* Добавлено Restart=on-failure. Нужно для того, чтобы программа перезапустилась после критической ошибки.
***
#### English
* Systemd service
* Changed WantedBy from multi-user.target to multi-user.target
* Removed ExecStop. It didn't actually work. To stop the service and clear the nftables rules, run the command: kor-elf-shield stop
* Added Restart=on-failure. This is necessary to ensure the program restarts after a critical error.
## 0.2.0 (29.11.2025)
***
#### Русский

View File

@@ -210,14 +210,3 @@ nftables = "/usr/sbin/nft"
# Default: /etc/kor-elf-shield/firewall.toml
###
firewall = "/etc/kor-elf-shield/firewall.toml"
###
# Укажите путь к настройкам уведомлений.
# Файл должен иметь расширение .toml.
# По умолчанию: /etc/kor-elf-shield/notifications.toml
# ***
# Specify the path to notification settings.
# The file must have the .toml extension.
# Default: /etc/kor-elf-shield/notifications.toml
###
notifications = "/etc/kor-elf-shield/notifications.toml"

View File

@@ -1,170 +0,0 @@
###############################################################################
# РАЗДЕЛ:Базовые настройки
# ***
# SECTION:Basic settings
###############################################################################
###
# Включает или выключает уведомления.
# !!! Не забудьте перед включением настроить email !!!
# false = Выключает.
# true = Включает.
#
# По умолчанию: false
# ***
# Turns notifications on or off.
# !!! Don't forget to set up your email before turning it on !!!
# false = Disables.
# true = Enables.
#
# Default: false
###
enabled = false
###
# Название сервера в уведомлениях
# По умолчанию: server
# ***
# Server name in notifications
# Default: server
###
server_name = "server"
###############################################################################
# РАЗДЕЛ:email
# ***
# SECTION:email
###############################################################################
[email]
###
# Сервер, через который будет отправляться почта.
# Например: smtp.gmail.com
# По умолчанию:
# ***
# The server through which mail will be sent.
# For example: smtp.gmail.com
# Default:
###
host = ""
###
# Указать порт сервера, через который будет отправляться почта.
# Например: 587
# По умолчанию:
# ***
# Specify the server port through which mail will be sent.
# For example: 587
# Default:
###
port = ""
###
# Логин к серверу, через который будет отправляться почта.
# По умолчанию:
# ***
# Login to the server through which mail will be sent.
# Default:
###
username = ""
###
# Пароль к серверу, через который будет отправляться почта.
# По умолчанию:
# ***
# Password for the server through which mail will be sent.
# Default:
###
password = ""
###
# Тип авторизации.
# Варианты: "PLAIN", "LOGIN", "CRAM-MD5", "NONE"
# Обычно используется "PLAIN". Если у вас внутренний релей без пароля - используйте "NONE".
# По умолчанию: "PLAIN"
# ***
# Authorization type.
# Options: "PLAIN", "LOGIN", "CRAM-MD5", "NONE"
# Usually "PLAIN" is used. If you have an internal relay without a password - use "NONE".
# Default: "PLAIN"
###
auth_type = "PLAIN"
###
# Защищённое соединение.
# Варианты: "NONE", "STARTTLS", "IMPLICIT"
#
# "NONE" — без TLS
# "STARTTLS" — обычный SMTP на 587 (или 25) + upgrade через STARTTLS
# "IMPLICIT" — SMTPS (TLS сразу), обычно 465
#
# По умолчанию: "STARTTLS"
# ***
# Secure connection.
# Options: "NONE", "STARTTLS", "IMPLICIT"
#
# "NONE" — without TLS
# "STARTTLS" — regular SMTP on 587 (or 25) + upgrade via STARTTLS
# "IMPLICIT" — SMTPS (TLS Immediately), typically 465
#
# Default: "STARTTLS"
###
tls_mode = "STARTTLS"
###
# Только если тип защищённого соединения в режиме starttls.
# Варианты: "MANDATORY", "OPPORTUNISTIC"
#
# "MANDATORY" — если STARTTLS недоступен/не удался будет вызвана ошибка
# "OPPORTUNISTIC" — попытаться STARTTLS, но если нельзя, то попытается отправить без TLS
#
# По умолчанию: "MANDATORY"
# ***
# Only if the secure connection type is in starttls mode.
# Options: "MANDATORY", "OPPORTUNISTIC"
#
# "MANDATORY" — if STARTTLS is unavailable/failed, an error will be raised
# "OPPORTUNISTIC" — try STARTTLS, but if that fails, it will try to send without TLS
#
# Default: "MANDATORY"
###
tls_policy = "MANDATORY"
###
# Проверять ли сертификат защищённого соединения.
#
# false = Выключает.
# true = Включает.
#
# По умолчанию: true
# ***
# Whether to check the secure connection certificate.
#
# false = Disables.
# true = Enables.
#
# Default: true
###
tls_verify = true
###
# Email, который будет указываться при отправки почты.
# Например: test@localhost
# По умолчанию:
# ***
# Email that will be specified when sending mail.
# For example: test@localhost
# Default:
###
from = ""
###
# Адрес электронной почты, на который будет отправлено письмо.
# Например: root@localhost
# По умолчанию:
# ***
# Email to whom the mail will be sent.
# For example: root@localhost
# Default:
###
to = ""

View File

@@ -3,10 +3,8 @@ Description=kor-elf-shield
After=network.target
[Service]
Type=simple
ExecStart=/usr/sbin/kor-elf-shield start
Restart=on-failure
RestartSec=10s
ExecStop=/usr/sbin/kor-elf-shield stop
[Install]
WantedBy=multi-user.target
WantedBy=sysinit.target

1
go.mod
View File

@@ -21,7 +21,6 @@ require (
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/wneessen/go-mail v0.7.2 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect

2
go.sum
View File

@@ -40,8 +40,6 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM=
github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=

View File

@@ -4,7 +4,6 @@ import (
"context"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting"
@@ -35,17 +34,6 @@ func runDaemon(ctx context.Context, _ *cli.Command) error {
_ = logger.Sync()
}()
notificationsConfig, err := setting.Config.OtherSettingsPath.ToNotificationsConfig()
if err != nil {
logger.Fatal(err.Error())
// Fatal should call os.Exit(1), but there's a chance that might not happen,
// so we return err just in case.
return err
}
notificationsClient := notifications.New(notificationsConfig, logger)
config, err := setting.Config.ToDaemonOptions()
if err != nil {
logger.Fatal(err.Error())
@@ -55,7 +43,7 @@ func runDaemon(ctx context.Context, _ *cli.Command) error {
return err
}
d, err := daemon.NewDaemon(config, logger, notificationsClient)
d, err := daemon.NewDaemon(config, logger)
if err != nil {
logger.Fatal(err.Error())

View File

@@ -6,7 +6,6 @@ import (
"time"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/pidfile"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/socket"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
@@ -18,11 +17,10 @@ type Daemon interface {
}
type daemon struct {
pidFile pidfile.PidFile
socket socket.Socket
logger log.Logger
firewall firewall.API
notifications notifications.Notifications
pidFile pidfile.PidFile
socket socket.Socket
logger log.Logger
firewall firewall.API
stopCh chan struct{}
}
@@ -54,11 +52,6 @@ func (d *daemon) Run(ctx context.Context, isTesting bool, testingInterval uint16
_ = d.socket.Close()
}()
d.notifications.Run()
defer func() {
_ = d.notifications.Close()
}()
go d.socket.Run(ctx, d.socketCommand)
d.runWorker(ctx, isTesting, testingInterval)

View File

@@ -1,43 +0,0 @@
package notifications
import (
"github.com/wneessen/go-mail"
)
type Config struct {
Enabled bool
ServerName string
Email Email
}
type Email struct {
Host string
Port uint
Username string
Password string
AuthType mail.SMTPAuthType
TLS TLS
From string
To string
}
type TLS struct {
Mode TLSMode
Policy TLSPolicy
Verify bool
}
type TLSMode string
const (
TLSModeNone TLSMode = "NONE"
TLSModeStartTLS TLSMode = "STARTTLS"
TLSModeImplicit TLSMode = "IMPLICIT"
)
type TLSPolicy string
const (
TLSPolicyMandatory TLSPolicy = "MANDATORY"
TLSPolicyOpportunistic TLSPolicy = "OPPORTUNISTIC"
)

View File

@@ -1,151 +0,0 @@
package notifications
import (
"context"
"crypto/tls"
"fmt"
"sync"
"time"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
"github.com/wneessen/go-mail"
)
type Message struct {
Subject string
Body string
}
type Notifications interface {
Run()
SendAsync(message Message)
Close() error
}
type notifications struct {
config Config
logger log.Logger
msgQueue chan Message
wg sync.WaitGroup
}
func New(config Config, logger log.Logger) Notifications {
return &notifications{
config: config,
logger: logger,
msgQueue: make(chan Message, 100),
}
}
func (n *notifications) Run() {
if n.config.Enabled == false {
n.logger.Info("Notifications are disabled")
}
n.wg.Add(1)
go func() {
defer n.wg.Done()
for msg := range n.msgQueue {
err := n.sendEmail(msg)
if err != nil {
n.logger.Error(fmt.Sprintf("failed to send email: %v", err))
} else if n.config.Enabled {
n.logger.Debug(fmt.Sprintf("email sent: Subject %s, Body %s", msg.Subject, msg.Body))
}
}
}()
}
func (n *notifications) SendAsync(message Message) {
select {
case n.msgQueue <- message:
if n.config.Enabled == false {
n.logger.Debug(fmt.Sprintf("email sending is disabled, message was added to the queue: Subject %s, Body %s", message.Subject, message.Body))
} else {
n.logger.Debug(fmt.Sprintf("added to the mail sending queue: Subject %s, Body %s", message.Subject, message.Body))
}
default:
n.logger.Error(fmt.Sprintf("failed to send email: queue is full"))
}
}
func (n *notifications) Close() error {
close(n.msgQueue)
n.logger.Debug("We are waiting for all notifications to be sent")
n.wg.Wait()
n.logger.Debug("Notifications queue processed and closed")
return nil
}
func (n *notifications) sendEmail(message Message) error {
if n.config.Enabled == false {
return nil
}
m := mail.NewMsg()
if err := m.From(n.config.Email.From); err != nil {
return err
}
if err := m.To(n.config.Email.To); err != nil {
return err
}
m.Subject(message.Subject + " (" + n.config.ServerName + ")")
m.SetBodyString(mail.TypeTextPlain, "Server: "+n.config.ServerName+"\n"+message.Body)
client, err := newClient(n.config.Email)
if err != nil {
return err
}
defer func() { _ = client.Close() }()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return client.DialAndSendWithContext(ctx, m)
}
func newClient(config Email) (*mail.Client, error) {
options := []mail.Option{
mail.WithPort(int(config.Port)),
mail.WithSMTPAuth(config.AuthType),
}
if config.AuthType != mail.SMTPAuthNoAuth {
options = append(options, mail.WithUsername(config.Username), mail.WithPassword(config.Password))
}
switch config.TLS.Mode {
case TLSModeImplicit:
options = append(options, mail.WithSSL())
break
case TLSModeStartTLS:
switch config.TLS.Policy {
case TLSPolicyMandatory:
options = append(options, mail.WithTLSPolicy(mail.TLSMandatory))
break
case TLSPolicyOpportunistic:
options = append(options, mail.WithTLSPolicy(mail.TLSOpportunistic))
break
default:
return nil, fmt.Errorf("unknown tls policy: %s", config.TLS.Policy)
}
if !config.TLS.Verify {
tlsCfg := &tls.Config{
InsecureSkipVerify: true,
}
options = append(options, mail.WithTLSConfig(tlsCfg))
}
break
case TLSModeNone:
break
default:
return nil, fmt.Errorf("unknown tls mode: %s", config.TLS.Mode)
}
options = append(options, mail.WithSSL())
return mail.NewClient(config.Host, options...)
}

View File

@@ -4,13 +4,12 @@ import (
"errors"
firewall2 "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/pidfile"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/socket"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
func NewDaemon(opts DaemonOptions, logger log.Logger, notifications notifications.Notifications) (Daemon, error) {
func NewDaemon(opts DaemonOptions, logger log.Logger) (Daemon, error) {
if logger == nil {
return nil, errors.New("logger is nil")
}
@@ -28,10 +27,9 @@ func NewDaemon(opts DaemonOptions, logger log.Logger, notifications notification
firewall, err := firewall2.New(opts.PathNftables, logger, opts.ConfigFirewall)
return &daemon{
pidFile: pidFile,
socket: sock,
logger: logger,
firewall: firewall,
notifications: notifications,
pidFile: pidFile,
socket: sock,
logger: logger,
firewall: firewall,
}, nil
}

View File

@@ -1,129 +0,0 @@
package notifications
import (
"errors"
"fmt"
"strings"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
"github.com/wneessen/go-mail"
)
type Email struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
AuthType string `mapstructure:"auth_type"`
TLSMode string `mapstructure:"tls_mode"`
TLSPolicy string `mapstructure:"tls_policy"`
TLSVerify bool `mapstructure:"tls_verify"`
From string `mapstructure:"from"`
To string `mapstructure:"to"`
}
func defaultEmail() Email {
return Email{
Host: "",
Port: 0,
Username: "",
Password: "",
AuthType: "PLAIN",
TLSMode: "STARTTLS",
TLSPolicy: "MANDATORY",
TLSVerify: true,
From: "",
To: "",
}
}
func (e Email) Validate() error {
if e.Host == "" {
return errors.New("host is not specified")
}
if e.Port == 0 {
return errors.New("port is not specified")
}
if err := validate.Port(e.Port, "port"); err != nil {
return err
}
if e.From == "" {
return errors.New("from is not specified")
}
if e.To == "" {
return errors.New("to is not specified")
}
if e.Username == "" {
return errors.New("username is not specified")
}
if e.Password == "" {
return errors.New("password is not specified")
}
return nil
}
func (e Email) ToTLSConfig() (notifications.TLS, error) {
mode, err := parseTLSMode(e.TLSMode)
if err != nil {
return notifications.TLS{}, err
}
policy, err := parseTLSPolicy(e.TLSPolicy)
if err != nil {
return notifications.TLS{}, err
}
return notifications.TLS{
Mode: mode,
Policy: policy,
Verify: e.TLSVerify,
}, nil
}
func ParseAuthType(authType string) (mail.SMTPAuthType, error) {
switch strings.ToUpper(authType) {
case "PLAIN":
return mail.SMTPAuthPlain, nil
case "LOGIN":
return mail.SMTPAuthLogin, nil
case "CRAM-MD5":
return mail.SMTPAuthCramMD5, nil
case "NONE":
return mail.SMTPAuthNoAuth, nil
}
return mail.SMTPAuthNoAuth, fmt.Errorf("unknown auth type: %s", authType)
}
func parseTLSMode(mode string) (notifications.TLSMode, error) {
switch strings.ToUpper(mode) {
case "STARTTLS":
return notifications.TLSModeStartTLS, nil
case "IMPLICIT":
return notifications.TLSModeImplicit, nil
case "NONE":
return notifications.TLSModeNone, nil
}
return notifications.TLSModeNone, fmt.Errorf("unknown tls mode: %s", mode)
}
func parseTLSPolicy(policy string) (notifications.TLSPolicy, error) {
switch strings.ToUpper(policy) {
case "MANDATORY":
return notifications.TLSPolicyMandatory, nil
case "OPPORTUNISTIC":
return notifications.TLSPolicyOpportunistic, nil
}
return notifications.TLSPolicyMandatory, fmt.Errorf("unknown tls policy: %s", policy)
}

View File

@@ -1,67 +0,0 @@
package notifications
import (
"errors"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
"github.com/spf13/viper"
)
type Setting struct {
Enabled bool `mapstructure:"enabled"`
ServerName string `mapstructure:"server_name"`
Email Email
}
func InitSetting(path string) (Setting, error) {
if err := validate.IsTomlFile(path, "otherSettingsPath.notifications"); err != nil {
return Setting{}, err
}
setting := settingDefault()
v := viper.New()
v.SetConfigType("toml")
v.SetConfigFile(path)
if err := v.ReadInConfig(); err != nil {
return Setting{}, err
}
if err := v.Unmarshal(&setting); err != nil {
return Setting{}, err
}
if !setting.Enabled {
return setting, nil
}
if err := setting.Validate(); err != nil {
return Setting{}, err
}
return setting, nil
}
func settingDefault() Setting {
return Setting{
Enabled: false,
ServerName: "server",
Email: defaultEmail(),
}
}
func (s Setting) Validate() error {
if s.ServerName == "" {
return errors.New("server_name is not specified")
}
if err := validate.NameWithDot(s.ServerName, "server_name"); err != nil {
return err
}
if err := s.Email.Validate(); err != nil {
return err
}
return nil
}

View File

@@ -2,21 +2,16 @@ package setting
import (
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
firewallSetting "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/firewall"
notificationsSetting "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/notifications"
"github.com/wneessen/go-mail"
)
type otherSettingsPath struct {
Firewall string `mapstructure:"firewall"`
Notifications string `mapstructure:"notifications"`
Firewall string `mapstructure:"firewall"`
}
func otherSettingsPathDefault() *otherSettingsPath {
return &otherSettingsPath{
Firewall: "/etc/kor-elf-shield/firewall.toml",
Notifications: "/etc/kor-elf-shield/notifications.toml",
Firewall: "/etc/kor-elf-shield/firewall.toml",
}
}
@@ -81,39 +76,3 @@ func (o *otherSettingsPath) ToFirewallConfig() (firewall.Config, error) {
Policy: configPolicy,
}, nil
}
func (o *otherSettingsPath) ToNotificationsConfig() (notifications.Config, error) {
setting, err := notificationsSetting.InitSetting(o.Notifications)
if err != nil {
return notifications.Config{}, err
}
authType := mail.SMTPAuthPlain
tls := notifications.TLS{}
if setting.Enabled {
authType, err = notificationsSetting.ParseAuthType(setting.Email.AuthType)
if err != nil {
return notifications.Config{}, err
}
tls, err = setting.Email.ToTLSConfig()
if err != nil {
return notifications.Config{}, err
}
}
return notifications.Config{
Enabled: setting.Enabled,
ServerName: setting.ServerName,
Email: notifications.Email{
Host: setting.Email.Host,
Port: uint(setting.Email.Port),
Username: setting.Email.Username,
Password: setting.Email.Password,
AuthType: authType,
TLS: tls,
From: setting.Email.From,
To: setting.Email.To,
},
}, nil
}

View File

@@ -54,17 +54,6 @@ func Name(name string, parameterName string) error {
return nil
}
func NameWithDot(name string, parameterName string) error {
if name == "" {
return fmt.Errorf("%s is empty", parameterName)
}
re := regexp.MustCompile(`^[a-zA-Z0-9_\-\.]{1,64}$`)
if !re.MatchString(name) {
return fmt.Errorf("%s must not contain special characters", parameterName)
}
return nil
}
func Port(port int, parameterName string) error {
if port < 0 || port > 65535 {
return fmt.Errorf("%s must be in range 0-65535", parameterName)