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:
|
||||
default: local
|
||||
rekey_timeout_interval: 125s
|
||||
local_resolvconf_prefix: tun.
|
||||
|
||||
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.
|
||||
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`
|
||||
- **Default:** `tun.`
|
||||
- **Environment Variable:** `WG_PORTAL_BACKEND_LOCAL_RESOLVCONF_PREFIX`
|
||||
|
||||
@@ -204,13 +204,13 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
|
||||
|
||||
// calculate if session was restarted
|
||||
p.UpdatedAt = now
|
||||
p.LastSessionStart = getSessionStartTime(*p, peer.BytesUpload, peer.BytesDownload,
|
||||
p.LastSessionStart = c.getSessionStartTime(*p, peer.BytesUpload, peer.BytesDownload,
|
||||
lastHandshake)
|
||||
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.Endpoint = peer.Endpoint
|
||||
p.LastHandshake = lastHandshake
|
||||
p.CalcConnected()
|
||||
p.CalcConnected(c.cfg.Backend.ReKeyTimeoutInterval)
|
||||
|
||||
if wasConnected != p.IsConnected {
|
||||
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,
|
||||
newReceived, newTransmitted uint64,
|
||||
latestHandshake *time.Time,
|
||||
@@ -258,7 +258,7 @@ func getSessionStartTime(
|
||||
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 {
|
||||
// old session was never initiated
|
||||
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.UpdatedAt = time.Now()
|
||||
p.CalcConnected()
|
||||
p.CalcConnected(c.cfg.Backend.ReKeyTimeoutInterval)
|
||||
|
||||
if wasConnected != p.IsConnected {
|
||||
connectionStateChanged = true
|
||||
|
||||
@@ -5,10 +5,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
func Test_getSessionStartTime(t *testing.T) {
|
||||
func TestStatisticsCollector_getSessionStartTime(t *testing.T) {
|
||||
now := time.Now()
|
||||
nowMinus1 := now.Add(-1 * time.Minute)
|
||||
nowMinus3 := now.Add(-3 * time.Minute)
|
||||
@@ -133,7 +134,14 @@ func Test_getSessionStartTime(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
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) {
|
||||
t.Errorf("getSessionStartTime() = %v, want %v", got, tt.want)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ const LocalBackendName = "local"
|
||||
type Backend struct {
|
||||
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
|
||||
|
||||
IgnoredLocalInterfaces []string `yaml:"ignored_local_interfaces"` // A list of interface names that should be ignored by this backend (e.g., "wg0")
|
||||
@@ -115,8 +117,8 @@ func (b *BackendMikrotik) GetApiTimeout() time.Duration {
|
||||
type BackendPfsense struct {
|
||||
BackendBase `yaml:",inline"` // Embed the base fields
|
||||
|
||||
ApiUrl string `yaml:"api_url"` // The base URL of the pfSense REST API (e.g., "https://pfsense.example.com/api/v2")
|
||||
ApiKey string `yaml:"api_key"` // API key for authentication (generated in pfSense under 'System' -> 'REST API' -> 'Keys')
|
||||
ApiUrl string `yaml:"api_url"` // The base URL of the pfSense REST API (e.g., "https://pfsense.example.com/api/v2")
|
||||
ApiKey string `yaml:"api_key"` // API key for authentication (generated in pfSense under 'System' -> 'REST API' -> 'Keys')
|
||||
ApiVerifyTls bool `yaml:"api_verify_tls"` // Whether to verify the TLS certificate of the pfSense API
|
||||
ApiTimeout time.Duration `yaml:"api_timeout"` // Timeout for API requests (default: 30 seconds)
|
||||
|
||||
|
||||
@@ -139,6 +139,7 @@ func defaultConfig() *Config {
|
||||
|
||||
cfg.Backend = Backend{
|
||||
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),
|
||||
// Most resolconf implementations use "tun." as a prefix for interface names.
|
||||
// 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"`
|
||||
}
|
||||
|
||||
func (s *PeerStatus) CalcConnected() {
|
||||
oldestHandshakeTime := time.Now().Add(-2 * time.Minute) // if a handshake is older than 2 minutes, the peer is no longer connected
|
||||
func (s *PeerStatus) CalcConnected(timeout time.Duration) {
|
||||
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
|
||||
if s.LastHandshake != nil {
|
||||
|
||||
@@ -9,11 +9,16 @@ func TestPeerStatus_IsConnected(t *testing.T) {
|
||||
now := time.Now()
|
||||
past := now.Add(-3 * 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 {
|
||||
name string
|
||||
status PeerStatus
|
||||
want bool
|
||||
name string
|
||||
status PeerStatus
|
||||
timeout time.Duration
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "Pingable and recent handshake",
|
||||
@@ -21,7 +26,8 @@ func TestPeerStatus_IsConnected(t *testing.T) {
|
||||
IsPingable: true,
|
||||
LastHandshake: &recent,
|
||||
},
|
||||
want: true,
|
||||
timeout: defaultTimeout,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Not pingable but recent handshake",
|
||||
@@ -29,7 +35,8 @@ func TestPeerStatus_IsConnected(t *testing.T) {
|
||||
IsPingable: false,
|
||||
LastHandshake: &recent,
|
||||
},
|
||||
want: true,
|
||||
timeout: defaultTimeout,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Pingable but old handshake",
|
||||
@@ -37,15 +44,44 @@ func TestPeerStatus_IsConnected(t *testing.T) {
|
||||
IsPingable: true,
|
||||
LastHandshake: &past,
|
||||
},
|
||||
want: true,
|
||||
timeout: defaultTimeout,
|
||||
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{
|
||||
IsPingable: false,
|
||||
LastHandshake: &past,
|
||||
},
|
||||
want: false,
|
||||
timeout: defaultTimeout,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Pingable and no handshake",
|
||||
@@ -53,7 +89,8 @@ func TestPeerStatus_IsConnected(t *testing.T) {
|
||||
IsPingable: true,
|
||||
LastHandshake: nil,
|
||||
},
|
||||
want: true,
|
||||
timeout: defaultTimeout,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Not pingable and no handshake",
|
||||
@@ -61,12 +98,13 @@ func TestPeerStatus_IsConnected(t *testing.T) {
|
||||
IsPingable: false,
|
||||
LastHandshake: nil,
|
||||
},
|
||||
want: false,
|
||||
timeout: defaultTimeout,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.status.CalcConnected()
|
||||
tt.status.CalcConnected(tt.timeout)
|
||||
if got := tt.status.IsConnected; got != tt.want {
|
||||
t.Errorf("IsConnected = %v, want %v", got, tt.want)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user