9 Commits
13 changed files with 470 additions and 1 deletions
+61
View File
@@ -508,6 +508,67 @@ forward_drop = "drop"
###
forward_priority = -10
###############################################################################
# РАЗДЕЛ:Port knocking
# ***
# SECTION:Port knocking
###############################################################################
###
# Тут можно настрить Port knocking. Это когда надо открыть порт, только поcле определённых стуков по определённому портам.
#
# Пример:
# [[portKnocking]]
# name = "ssh" # Имя должно быть уникальное и разрешены символы: "a-z, A-Z, -, _"
# port = 22 # Номер порта, который нужно открыть после всех стуков
# protocol = "tcp" # Протокол: tcp, udp
# ip_version = "ip4" # Версия IP: ip4, ip6
# [[portKnocking.knock]] # Первый стук
# port = 2222 # Порт стука
# protocol = "tcp" # Протокол: tcp, udp
# timeout = 30 # Время в секундах на которое работает стук
# action = "drop" # Во время стука, какой ответ отдавать: accept, return, drop or reject
## Лучше установить для параметра "action" значение "drop", чтобы любой, кто попытается угадать такие порты для "стука",
## не смог отличить их от заблокированного порта.
# [[portKnocking.knock]] # Второй стук (можно добавлять сколько хотите)
# port = 2225
# protocol = "tcp"
# timeout = 30
# action = "drop"
#
# Рассмотрим пример. Мы назвали настройку для portKnocking "ssh". В nftables будут созданны наборы sets таким образом:
# knock_ssh_0, knock_ssh_1. Мы открываем 22/tcp порт для IP адреса, который прошёл в данном случае два стука.
# 1 стук надо сделать на 2222/tcp и в течении 30 секунд надо сделать второй стук на порт 2225/tcp.
# После второго стука будет открыт для данного IP адреса порт 22/tcp на 30 секунд.
#
# ***
#
# Here you can configure port knocking. This allows you to open a port only after certain knocks on a specific ports.
#
# Example:
# [[portKnocking]]
# name = "ssh" # The name must be unique and symbols are allowed: "a-z, A-Z, -, _"
# port = 22 # The port number that needs to be opened after all the knocking
# protocol = "tcp" # Protocol: tcp, udp
# ip_version = "ip4" # IP version: ip4, ip6
# [[portKnocking.knock]] # The first knock
# port = 2222 # Port of knocking
# protocol = "tcp" # Protocol: tcp, udp
# timeout = 30 # The time in seconds for which the knocking works
# action = "drop" # When knocking, what answer should I give: accept, return, drop or reject
## It's best to set the "action" parameter to "drop" so that anyone trying to guess such ports for "knocking"
## won't be able to distinguish them from a blocked port.
# [[portKnocking.knock]] # Second knock (you can add as many as you want)
# port = 2225
# protocol = "tcp"
# timeout = 30
# action = "drop"
#
# Let's look at an example. We named the portKnocking setting "ssh." Sets will be created in nftables as follows:
# knock_ssh_0, knock_ssh_1. We open port 22/tcp for the IP address that, in this case, has been knocked twice.
# The first knock should be made on 2222/tcp, and within 30 seconds, a second knock should be made on port 2225/tcp.
# After the second knock, port 22/tcp will be opened for this IP address for 30 seconds.
###
###############################################################################
# РАЗДЕЛ:Именование метаданных
# ***
@@ -0,0 +1,20 @@
package block
import (
"strconv"
"strings"
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client"
"git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/ip"
)
func NewPortKnocking(nft nft.NFT, family family.Type, table string, name string, ipVersion ip.Version, timeout uint32) error {
params := []string{"type", ipVersion.ToNftForSet() + ";", "flags timeout; timeout", strconv.Itoa(int(timeout)) + "s;"}
_, err := newList(nft, family, table, name, strings.Join(params, " "))
if err != nil {
return err
}
return nil
}
+10
View File
@@ -44,6 +44,7 @@ type Chains interface {
NewBlockListIP(name string) (block.ListIP, error)
NewBlockListIPWithPort(name string) (block.ListIPWithPort, error)
NewBlocklist(name string) (block.Blocklist, error)
NewPortKnocking(name string) (PortKnocking, error)
}
type chains struct {
@@ -255,6 +256,15 @@ func (c *chains) NewBlocklist(name string) (block.Blocklist, error) {
return blockList, nil
}
func (c *chains) NewPortKnocking(name string) (PortKnocking, error) {
portKnocking, err := newPortKnocking(c.nft, c.family, c.table, name)
if err != nil {
return nil, err
}
return portKnocking, nil
}
func clearRules(nft nft.NFT, family nftFamily.Type, table string) error {
if err := nft.Table().Delete(family, table); err != nil {
if !strings.Contains(string(err.Error()), "delete table "+family.String()+" "+table) {
@@ -0,0 +1,91 @@
package chain
import (
"strconv"
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client"
nftChain "git.kor-elf.net/kor-elf-shield/go-nftables-client/chain"
"git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/chain/block"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/types"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/ip"
)
type PortKnocking interface {
AddFirstStageRule(
name string,
ipVersion ip.Version,
l4Port types.L4Port,
timeout uint32,
action types.KnockAction,
) error
AddNextStageRule(
prevName, nextName string,
ipVersion ip.Version,
l4Port types.L4Port,
timeout uint32,
action types.KnockAction,
) error
AddRuleIn(AddRuleFunc func(expr ...string) error) error
}
type portKnocking struct {
nft nft.NFT
family family.Type
table string
chain string
}
func newPortKnocking(nft nft.NFT, family family.Type, table string, chain string) (PortKnocking, error) {
if err := nft.Chain().Add(family, table, chain, nftChain.TypeNone); err != nil {
return nil, err
}
return &portKnocking{
nft: nft,
family: family,
table: table,
chain: chain,
}, nil
}
func (k *portKnocking) AddRuleIn(AddRuleFunc func(expr ...string) error) error {
return AddRuleFunc("iifname != \"lo\" counter jump " + k.chain)
}
func (k *portKnocking) AddFirstStageRule(
name string,
ipVersion ip.Version,
l4Port types.L4Port,
timeout uint32,
action types.KnockAction,
) error {
if err := block.NewPortKnocking(k.nft, k.family, k.table, name, ipVersion, timeout); err != nil {
return err
}
expr := []string{
l4Port.ProtocolString(), "dport", l4Port.NumberString(), "add", "@" + name,
"{", ipVersion.ToNft(), "saddr timeout", strconv.Itoa(int(timeout)) + "s", "}", action.String(),
}
return k.nft.Rule().Add(k.family, k.table, k.chain, expr...)
}
func (k *portKnocking) AddNextStageRule(
prevName, nextName string,
ipVersion ip.Version,
l4Port types.L4Port,
timeout uint32,
action types.KnockAction,
) error {
if err := block.NewPortKnocking(k.nft, k.family, k.table, nextName, ipVersion, timeout); err != nil {
return err
}
expr := []string{
ipVersion.ToNft(), "saddr", "@" + prevName,
l4Port.ProtocolString(), "dport", l4Port.NumberString(), "add", "@" + nextName,
"{", ipVersion.ToNft(), "saddr}", action.String(),
}
return k.nft.Rule().Add(k.family, k.table, k.chain, expr...)
}
+15
View File
@@ -2,6 +2,7 @@ package firewall
import (
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/types"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/ip"
)
type Config struct {
@@ -12,6 +13,7 @@ type Config struct {
Options ConfigOptions
MetadataNaming ConfigMetadata
Policy ConfigPolicy
PortKnocking []ConfigPortKnocking
}
type ConfigOptions struct {
@@ -74,6 +76,19 @@ type ConfigIP struct {
LimitRate string
}
type ConfigPortKnocking struct {
Name string
Port types.L4Port
IPVersion ip.Version
Knocks []*ConfigKnock
}
type ConfigKnock struct {
Port types.L4Port
Action types.KnockAction
Timeout uint32
}
type ClearMode int8
const (
+48
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"net"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/chain"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg"
)
@@ -247,6 +248,10 @@ func (f *firewall) reloadInputAddIPs() error {
return err
}
if err := f.reloadPortKnocking(chain); err != nil {
return err
}
for _, ipConfig := range f.config.IP4.InIPs {
if err := inputAddIP(chain.AddRule, ipConfig, "ip"); err != nil {
return err
@@ -265,6 +270,49 @@ func (f *firewall) reloadInputAddIPs() error {
return nil
}
func (f *firewall) reloadPortKnocking(chain chain.LocalInput) error {
if len(f.config.PortKnocking) == 0 {
return nil
}
portKnocking, err := f.chains.NewPortKnocking("port_knocking")
if err != nil {
return err
}
for _, portKnockingConfig := range f.config.PortKnocking {
var knockName, prevKnockName string
for index, knock := range portKnockingConfig.Knocks {
prevKnockName = knockName
knockName = fmt.Sprintf("knock_%s_%d", portKnockingConfig.Name, index)
if index == 0 {
if err := portKnocking.AddFirstStageRule(knockName, portKnockingConfig.IPVersion, knock.Port, knock.Timeout, knock.Action); err != nil {
return err
}
continue
}
if err := portKnocking.AddNextStageRule(prevKnockName, knockName, portKnockingConfig.IPVersion, knock.Port, knock.Timeout, knock.Action); err != nil {
return err
}
}
expr := []string{
portKnockingConfig.IPVersion.ToNft(), "saddr", "@" + knockName,
portKnockingConfig.Port.ProtocolString(), "dport", portKnockingConfig.Port.NumberString(), "accept",
}
if err := chain.AddRule(expr...); err != nil {
return err
}
}
if err := portKnocking.AddRuleIn(chain.AddRule); err != nil {
return err
}
return nil
}
func inputAddIP(addRuleFunc func(expr ...string) error, config ConfigIP, ipMatch string) error {
rule := ipMatch + " saddr " + config.IP + " iifname != \"lo\""
+24
View File
@@ -41,6 +41,30 @@ func (a Action) String() string {
}
}
type KnockAction int8
const (
KnockActionAccept KnockAction = iota + 1
KnockActionReject
KnockActionDrop
KnockActionReturn
)
func (a KnockAction) String() string {
switch a {
case KnockActionAccept:
return "accept"
case KnockActionReject:
return "reject"
case KnockActionDrop:
return "drop"
case KnockActionReturn:
return "return"
default:
return "drop"
}
}
type Protocol int8
const (
+11
View File
@@ -23,6 +23,17 @@ func (v Version) ToNft() string {
}
}
func (v Version) ToNftForSet() string {
switch v {
case IPv4:
return "ipv4_addr"
case IPv6:
return "ipv6_addr"
default:
return "unknown"
}
}
func DetermineIPVersion(ip string) (ipNet string, version Version, err error) {
ipNet, version, err = parseCIDR(ip)
if err != nil {
+15
View File
@@ -41,3 +41,18 @@ func ToAction(action string) (types.Action, error) {
return types.ActionAccept, errors.New("invalid action. Must be accept, drop or reject")
}
}
func ToKnockAction(action string) (types.KnockAction, error) {
switch strings.ToLower(action) {
case "accept":
return types.KnockActionAccept, nil
case "drop":
return types.KnockActionDrop, nil
case "reject":
return types.KnockActionReject, nil
case "return":
return types.KnockActionReturn, nil
default:
return types.KnockActionDrop, errors.New("invalid action. Must be accept, return, drop or reject")
}
}
+26
View File
@@ -1,6 +1,8 @@
package firewall
import (
"fmt"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
"github.com/spf13/viper"
@@ -14,6 +16,7 @@ type Setting struct {
Options options
MetadataNaming metadataNaming
Policy policy
PortKnocking []portKnocking
}
func InitSetting(path string) (Setting, error) {
@@ -49,6 +52,7 @@ func settingDefault() Setting {
Options: defaultOptions(),
MetadataNaming: defaultMetadataNaming(),
Policy: defaultPolicy(),
PortKnocking: defaultPortKnocking(),
}
}
@@ -101,3 +105,25 @@ func (s Setting) ToIPs() (IPs IPs, error error) {
return
}
func (s Setting) ToConfigPortKnocking() ([]firewall.ConfigPortKnocking, error) {
fmt.Println(s.PortKnocking)
var configPortKnocking []firewall.ConfigPortKnocking
portKnockingNames := make(map[string]string)
for _, portKnocking := range s.PortKnocking {
if _, ok := portKnockingNames[portKnocking.Name]; ok {
return nil, fmt.Errorf("port knocking name %s is duplicated", portKnocking.Name)
}
portKnockingNames[portKnocking.Name] = portKnocking.Name
addPortKnocking, err := portKnocking.ToPortKnocking()
if err != nil {
return nil, err
}
configPortKnocking = append(configPortKnocking, addPortKnocking)
}
return configPortKnocking, nil
}
@@ -0,0 +1,87 @@
package firewall
import (
"fmt"
"strings"
"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/firewall/types"
port2 "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/ip"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
)
type portKnocking struct {
Name string `mapstructure:"name"`
IPVersion string `mapstructure:"ip_version"`
Port int `mapstructure:"port"`
Protocol string `mapstructure:"protocol"`
Knocks []portKnockingKnock `mapstructure:"knock"`
}
func defaultPortKnocking() []portKnocking {
return []portKnocking{}
}
func (p *portKnocking) ToPortKnocking() (firewall.ConfigPortKnocking, error) {
if len(p.Knocks) == 0 {
return firewall.ConfigPortKnocking{}, fmt.Errorf("port knocking must have at least one knock")
}
if err := p.validate(); err != nil {
return firewall.ConfigPortKnocking{}, err
}
protocol, err := port2.ToProtocol(p.Protocol)
if err != nil {
return firewall.ConfigPortKnocking{}, err
}
l4Port, err := types.NewL4Port(uint16(p.Port), protocol)
if err != nil {
return firewall.ConfigPortKnocking{}, err
}
ipVersion, err := toVersionIP(p.IPVersion)
if err != nil {
return firewall.ConfigPortKnocking{}, err
}
knocks := make([]*firewall.ConfigKnock, 0, len(p.Knocks))
for _, knock := range p.Knocks {
knock, err := knock.ToKnock()
if err != nil {
return firewall.ConfigPortKnocking{}, err
}
knocks = append(knocks, &knock)
}
return firewall.ConfigPortKnocking{
Name: p.Name,
Port: l4Port,
IPVersion: ipVersion,
Knocks: knocks,
}, nil
}
func (p *portKnocking) validate() error {
if err := validate.Name(p.Name, "portKnocking.name"); err != nil {
return err
}
if err := validate.Port(p.Port, "portKnocking.port"); err != nil {
return err
}
return nil
}
func toVersionIP(versionIP string) (port2.Version, error) {
switch strings.ToLower(versionIP) {
case "ip4":
return port2.IPv4, nil
case "ip6":
return port2.IPv6, nil
default:
return port2.IPv4, fmt.Errorf("invalid version_ip. Must be ip4 or ip6")
}
}
@@ -0,0 +1,55 @@
package firewall
import (
"fmt"
"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/firewall/types"
port2 "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/ip"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
)
type portKnockingKnock struct {
Port int `mapstructure:"port"`
Protocol string `mapstructure:"protocol"`
Timeout int32 `mapstructure:"timeout"`
Action string `mapstructure:"action"`
}
func (k *portKnockingKnock) ToKnock() (firewall.ConfigKnock, error) {
if err := k.validate(); err != nil {
return firewall.ConfigKnock{}, err
}
protocol, err := port2.ToProtocol(k.Protocol)
if err != nil {
return firewall.ConfigKnock{}, err
}
l4Port, err := types.NewL4Port(uint16(k.Port), protocol)
if err != nil {
return firewall.ConfigKnock{}, err
}
action, err := port2.ToKnockAction(k.Action)
if err != nil {
return firewall.ConfigKnock{}, err
}
return firewall.ConfigKnock{
Port: l4Port,
Action: action,
Timeout: uint32(k.Timeout),
}, nil
}
func (k *portKnockingKnock) validate() error {
if err := validate.Port(k.Port, "knock.port"); err != nil {
return err
}
if k.Timeout <= 0 {
return fmt.Errorf("knock.timeout must be positive")
}
return nil
}
+7 -1
View File
@@ -63,6 +63,11 @@ func (o *otherSettingsPath) ToFirewallConfig(dockerSupport bool) (firewall.Confi
return firewall.Config{}, err
}
portKnocking, err := setting.ToConfigPortKnocking()
if err != nil {
return firewall.Config{}, err
}
return firewall.Config{
InPorts: inPorts,
OutPorts: outPorts,
@@ -96,7 +101,8 @@ func (o *otherSettingsPath) ToFirewallConfig(dockerSupport bool) (firewall.Confi
ChainOutputName: setting.MetadataNaming.ChainOutputName,
ChainForwardName: setting.MetadataNaming.ChainForwardName,
},
Policy: configPolicy,
Policy: configPolicy,
PortKnocking: portKnocking,
}, nil
}