mirror of
https://github.com/h44z/wg-portal.git
synced 2026-03-24 00:56:26 +00:00
fix: configurable handshake validity interval and improved defaults (#645)
* fix: support configurable rekey timeout interval for peer connectivity tracking (#641) * change default check-time to 180s
This commit is contained in:
@@ -28,6 +28,7 @@ core:
|
|||||||
|
|
||||||
backend:
|
backend:
|
||||||
default: local
|
default: local
|
||||||
|
rekey_timeout_interval: 125s
|
||||||
local_resolvconf_prefix: tun.
|
local_resolvconf_prefix: tun.
|
||||||
|
|
||||||
advanced:
|
advanced:
|
||||||
@@ -203,6 +204,13 @@ The current MikroTik backend is in **BETA** and may not support all features.
|
|||||||
- **Description:** The default backend to use for managing WireGuard interfaces.
|
- **Description:** The default backend to use for managing WireGuard interfaces.
|
||||||
Valid options are: `local`, or other backend id's configured in the `mikrotik` section.
|
Valid options are: `local`, or other backend id's configured in the `mikrotik` section.
|
||||||
|
|
||||||
|
### `rekey_timeout_interval`
|
||||||
|
- **Default:** `180s`
|
||||||
|
- **Environment Variable:** `WG_PORTAL_BACKEND_REKEY_TIMEOUT_INTERVAL`
|
||||||
|
- **Description:** The interval after which a WireGuard peer is considered disconnected if no handshake updates are received.
|
||||||
|
This corresponds to the WireGuard rekey timeout setting of 120 seconds plus a 60-second buffer to account for latency or retry handling.
|
||||||
|
Uses Go duration format (e.g., `10s`, `1m`). If omitted, a default of 180 seconds is used.
|
||||||
|
|
||||||
### `local_resolvconf_prefix`
|
### `local_resolvconf_prefix`
|
||||||
- **Default:** `tun.`
|
- **Default:** `tun.`
|
||||||
- **Environment Variable:** `WG_PORTAL_BACKEND_LOCAL_RESOLVCONF_PREFIX`
|
- **Environment Variable:** `WG_PORTAL_BACKEND_LOCAL_RESOLVCONF_PREFIX`
|
||||||
|
|||||||
@@ -204,13 +204,13 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
|
|||||||
|
|
||||||
// calculate if session was restarted
|
// calculate if session was restarted
|
||||||
p.UpdatedAt = now
|
p.UpdatedAt = now
|
||||||
p.LastSessionStart = getSessionStartTime(*p, peer.BytesUpload, peer.BytesDownload,
|
p.LastSessionStart = c.getSessionStartTime(*p, peer.BytesUpload, peer.BytesDownload,
|
||||||
lastHandshake)
|
lastHandshake)
|
||||||
p.BytesReceived = peer.BytesUpload // store bytes that where uploaded from the peer and received by the server
|
p.BytesReceived = peer.BytesUpload // store bytes that where uploaded from the peer and received by the server
|
||||||
p.BytesTransmitted = peer.BytesDownload // store bytes that where received from the peer and sent by the server
|
p.BytesTransmitted = peer.BytesDownload // store bytes that where received from the peer and sent by the server
|
||||||
p.Endpoint = peer.Endpoint
|
p.Endpoint = peer.Endpoint
|
||||||
p.LastHandshake = lastHandshake
|
p.LastHandshake = lastHandshake
|
||||||
p.CalcConnected()
|
p.CalcConnected(c.cfg.Backend.ReKeyTimeoutInterval)
|
||||||
|
|
||||||
if wasConnected != p.IsConnected {
|
if wasConnected != p.IsConnected {
|
||||||
slog.Debug("peer connection state changed",
|
slog.Debug("peer connection state changed",
|
||||||
@@ -249,7 +249,7 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSessionStartTime(
|
func (c *StatisticsCollector) getSessionStartTime(
|
||||||
oldStats domain.PeerStatus,
|
oldStats domain.PeerStatus,
|
||||||
newReceived, newTransmitted uint64,
|
newReceived, newTransmitted uint64,
|
||||||
latestHandshake *time.Time,
|
latestHandshake *time.Time,
|
||||||
@@ -258,7 +258,7 @@ func getSessionStartTime(
|
|||||||
return nil // currently not connected
|
return nil // currently not connected
|
||||||
}
|
}
|
||||||
|
|
||||||
oldestHandshakeTime := time.Now().Add(-2 * time.Minute) // if a handshake is older than 2 minutes, the peer is no longer connected
|
oldestHandshakeTime := time.Now().Add(-1 * c.cfg.Backend.ReKeyTimeoutInterval) // if a handshake is older than the rekey interval + grace-period, the peer is no longer connected
|
||||||
switch {
|
switch {
|
||||||
// old session was never initiated
|
// old session was never initiated
|
||||||
case oldStats.BytesReceived == 0 && oldStats.BytesTransmitted == 0 && (newReceived > 0 || newTransmitted > 0):
|
case oldStats.BytesReceived == 0 && oldStats.BytesTransmitted == 0 && (newReceived > 0 || newTransmitted > 0):
|
||||||
@@ -369,7 +369,7 @@ func (c *StatisticsCollector) pingWorker(ctx context.Context) {
|
|||||||
p.LastPing = nil
|
p.LastPing = nil
|
||||||
}
|
}
|
||||||
p.UpdatedAt = time.Now()
|
p.UpdatedAt = time.Now()
|
||||||
p.CalcConnected()
|
p.CalcConnected(c.cfg.Backend.ReKeyTimeoutInterval)
|
||||||
|
|
||||||
if wasConnected != p.IsConnected {
|
if wasConnected != p.IsConnected {
|
||||||
connectionStateChanged = true
|
connectionStateChanged = true
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_getSessionStartTime(t *testing.T) {
|
func TestStatisticsCollector_getSessionStartTime(t *testing.T) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
nowMinus1 := now.Add(-1 * time.Minute)
|
nowMinus1 := now.Add(-1 * time.Minute)
|
||||||
nowMinus3 := now.Add(-3 * time.Minute)
|
nowMinus3 := now.Add(-3 * time.Minute)
|
||||||
@@ -133,7 +134,14 @@ func Test_getSessionStartTime(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := getSessionStartTime(tt.args.oldStats, tt.args.newReceived, tt.args.newTransmitted,
|
c := &StatisticsCollector{
|
||||||
|
cfg: &config.Config{
|
||||||
|
Backend: config.Backend{
|
||||||
|
ReKeyTimeoutInterval: 180 * time.Second,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if got := c.getSessionStartTime(tt.args.oldStats, tt.args.newReceived, tt.args.newTransmitted,
|
||||||
tt.args.lastHandshake); !reflect.DeepEqual(got, tt.want) {
|
tt.args.lastHandshake); !reflect.DeepEqual(got, tt.want) {
|
||||||
t.Errorf("getSessionStartTime() = %v, want %v", got, tt.want)
|
t.Errorf("getSessionStartTime() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ const LocalBackendName = "local"
|
|||||||
type Backend struct {
|
type Backend struct {
|
||||||
Default string `yaml:"default"` // The default backend to use (defaults to the internal backend)
|
Default string `yaml:"default"` // The default backend to use (defaults to the internal backend)
|
||||||
|
|
||||||
|
ReKeyTimeoutInterval time.Duration `yaml:"rekey_timeout_interval"` // Interval after which a connection is assumed dead
|
||||||
|
|
||||||
// Local Backend-specific configuration
|
// Local Backend-specific configuration
|
||||||
|
|
||||||
IgnoredLocalInterfaces []string `yaml:"ignored_local_interfaces"` // A list of interface names that should be ignored by this backend (e.g., "wg0")
|
IgnoredLocalInterfaces []string `yaml:"ignored_local_interfaces"` // A list of interface names that should be ignored by this backend (e.g., "wg0")
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ func defaultConfig() *Config {
|
|||||||
|
|
||||||
cfg.Backend = Backend{
|
cfg.Backend = Backend{
|
||||||
Default: LocalBackendName, // local backend is the default (using wgcrtl)
|
Default: LocalBackendName, // local backend is the default (using wgcrtl)
|
||||||
|
ReKeyTimeoutInterval: getEnvDuration("WG_PORTAL_BACKEND_REKEY_TIMEOUT_INTERVAL", 180*time.Second),
|
||||||
IgnoredLocalInterfaces: getEnvStrSlice("WG_PORTAL_BACKEND_IGNORED_LOCAL_INTERFACES", nil),
|
IgnoredLocalInterfaces: getEnvStrSlice("WG_PORTAL_BACKEND_IGNORED_LOCAL_INTERFACES", nil),
|
||||||
// Most resolconf implementations use "tun." as a prefix for interface names.
|
// Most resolconf implementations use "tun." as a prefix for interface names.
|
||||||
// But systemd's implementation uses no prefix, for example.
|
// But systemd's implementation uses no prefix, for example.
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ type PeerStatus struct {
|
|||||||
LastSessionStart *time.Time `gorm:"column:last_session_start" json:"LastSessionStart"`
|
LastSessionStart *time.Time `gorm:"column:last_session_start" json:"LastSessionStart"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PeerStatus) CalcConnected() {
|
func (s *PeerStatus) CalcConnected(timeout time.Duration) {
|
||||||
oldestHandshakeTime := time.Now().Add(-2 * time.Minute) // if a handshake is older than 2 minutes, the peer is no longer connected
|
oldestHandshakeTime := time.Now().Add(-1 * timeout) // if a handshake is older than the rekey-interval + grace-period, the peer is no longer connected
|
||||||
|
|
||||||
handshakeValid := false
|
handshakeValid := false
|
||||||
if s.LastHandshake != nil {
|
if s.LastHandshake != nil {
|
||||||
|
|||||||
@@ -9,10 +9,15 @@ func TestPeerStatus_IsConnected(t *testing.T) {
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
past := now.Add(-3 * time.Minute)
|
past := now.Add(-3 * time.Minute)
|
||||||
recent := now.Add(-1 * time.Minute)
|
recent := now.Add(-1 * time.Minute)
|
||||||
|
defaultTimeout := 125 * time.Second // rekey interval of 120s + 5 seconds grace period
|
||||||
|
past126 := now.Add(-1*defaultTimeout - 1*time.Second)
|
||||||
|
past125 := now.Add(-1 * defaultTimeout)
|
||||||
|
past124 := now.Add(-1*defaultTimeout + 1*time.Second)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
status PeerStatus
|
status PeerStatus
|
||||||
|
timeout time.Duration
|
||||||
want bool
|
want bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -21,6 +26,7 @@ func TestPeerStatus_IsConnected(t *testing.T) {
|
|||||||
IsPingable: true,
|
IsPingable: true,
|
||||||
LastHandshake: &recent,
|
LastHandshake: &recent,
|
||||||
},
|
},
|
||||||
|
timeout: defaultTimeout,
|
||||||
want: true,
|
want: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -29,6 +35,7 @@ func TestPeerStatus_IsConnected(t *testing.T) {
|
|||||||
IsPingable: false,
|
IsPingable: false,
|
||||||
LastHandshake: &recent,
|
LastHandshake: &recent,
|
||||||
},
|
},
|
||||||
|
timeout: defaultTimeout,
|
||||||
want: true,
|
want: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -37,14 +44,43 @@ func TestPeerStatus_IsConnected(t *testing.T) {
|
|||||||
IsPingable: true,
|
IsPingable: true,
|
||||||
LastHandshake: &past,
|
LastHandshake: &past,
|
||||||
},
|
},
|
||||||
|
timeout: defaultTimeout,
|
||||||
want: true,
|
want: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Not pingable and old handshake",
|
name: "Not pingable and ok handshake (-124s)",
|
||||||
|
status: PeerStatus{
|
||||||
|
IsPingable: false,
|
||||||
|
LastHandshake: &past124,
|
||||||
|
},
|
||||||
|
timeout: defaultTimeout,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Not pingable and old handshake (-125s)",
|
||||||
|
status: PeerStatus{
|
||||||
|
IsPingable: false,
|
||||||
|
LastHandshake: &past125,
|
||||||
|
},
|
||||||
|
timeout: defaultTimeout,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Not pingable and old handshake (-126s)",
|
||||||
|
status: PeerStatus{
|
||||||
|
IsPingable: false,
|
||||||
|
LastHandshake: &past126,
|
||||||
|
},
|
||||||
|
timeout: defaultTimeout,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Not pingable and old handshake (very old)",
|
||||||
status: PeerStatus{
|
status: PeerStatus{
|
||||||
IsPingable: false,
|
IsPingable: false,
|
||||||
LastHandshake: &past,
|
LastHandshake: &past,
|
||||||
},
|
},
|
||||||
|
timeout: defaultTimeout,
|
||||||
want: false,
|
want: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -53,6 +89,7 @@ func TestPeerStatus_IsConnected(t *testing.T) {
|
|||||||
IsPingable: true,
|
IsPingable: true,
|
||||||
LastHandshake: nil,
|
LastHandshake: nil,
|
||||||
},
|
},
|
||||||
|
timeout: defaultTimeout,
|
||||||
want: true,
|
want: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -61,12 +98,13 @@ func TestPeerStatus_IsConnected(t *testing.T) {
|
|||||||
IsPingable: false,
|
IsPingable: false,
|
||||||
LastHandshake: nil,
|
LastHandshake: nil,
|
||||||
},
|
},
|
||||||
|
timeout: defaultTimeout,
|
||||||
want: false,
|
want: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
tt.status.CalcConnected()
|
tt.status.CalcConnected(tt.timeout)
|
||||||
if got := tt.status.IsConnected; got != tt.want {
|
if got := tt.status.IsConnected; got != tt.want {
|
||||||
t.Errorf("IsConnected = %v, want %v", got, tt.want)
|
t.Errorf("IsConnected = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user