mirror of
https://github.com/h44z/wg-portal.git
synced 2026-04-10 17:36:29 +00:00
Compare commits
3 Commits
v2.2.1
...
wg_check_i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bfbe12d5b | ||
|
|
c3defbf675 | ||
|
|
9c56e92443 |
@@ -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`
|
||||||
|
|||||||
@@ -35,6 +35,14 @@ WireGuard Portal supports managing WireGuard interfaces through three distinct d
|
|||||||
> :warning: If host networking is used, the WireGuard Portal UI will be accessible on all the host's IP addresses if the listening address is set to `:8888` in the configuration file.
|
> :warning: If host networking is used, the WireGuard Portal UI will be accessible on all the host's IP addresses if the listening address is set to `:8888` in the configuration file.
|
||||||
To avoid this, you can bind the listening address to a specific IP address, for example, the loopback address (`127.0.0.1:8888`). It is also possible to deploy firewall rules to restrict access to the WireGuard Portal UI.
|
To avoid this, you can bind the listening address to a specific IP address, for example, the loopback address (`127.0.0.1:8888`). It is also possible to deploy firewall rules to restrict access to the WireGuard Portal UI.
|
||||||
|
|
||||||
|
> :warning: If the host is running **systemd-networkd**, routes managed by WireGuard Portal may be removed whenever systemd-networkd restarts, as it will clean up routes it considers "foreign". To prevent this, add the following to your host's network configuration (e.g. `/etc/systemd/networkd.conf` or a drop-in file):
|
||||||
|
> ```ini
|
||||||
|
> [Network]
|
||||||
|
> ManageForeignRoutingPolicyRules=no
|
||||||
|
> ManageForeignRoutes=no
|
||||||
|
> ```
|
||||||
|
> After editing, reload the configuration with `sudo systemctl restart systemd-networkd`. For more information refer to the [systemd-networkd documentation](https://www.freedesktop.org/software/systemd/man/latest/networkd.conf.html#ManageForeignRoutes=).
|
||||||
|
|
||||||
- **Within the WireGuard Portal Docker container**:
|
- **Within the WireGuard Portal Docker container**:
|
||||||
WireGuard interfaces can be managed directly from within the WireGuard Portal container itself.
|
WireGuard interfaces can be managed directly from within the WireGuard Portal container itself.
|
||||||
This is the recommended approach when running WireGuard Portal via Docker, as it encapsulates all functionality in a single, portable container without requiring a separate WireGuard host or image.
|
This is the recommended approach when running WireGuard Portal via Docker, as it encapsulates all functionality in a single, portable container without requiring a separate WireGuard host or image.
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -115,8 +117,8 @@ func (b *BackendMikrotik) GetApiTimeout() time.Duration {
|
|||||||
type BackendPfsense struct {
|
type BackendPfsense struct {
|
||||||
BackendBase `yaml:",inline"` // Embed the base fields
|
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")
|
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')
|
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
|
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)
|
ApiTimeout time.Duration `yaml:"api_timeout"` // Timeout for API requests (default: 30 seconds)
|
||||||
|
|
||||||
|
|||||||
@@ -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,11 +9,16 @@ 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
|
||||||
want bool
|
timeout time.Duration
|
||||||
|
want bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Pingable and recent handshake",
|
name: "Pingable and recent handshake",
|
||||||
@@ -21,7 +26,8 @@ func TestPeerStatus_IsConnected(t *testing.T) {
|
|||||||
IsPingable: true,
|
IsPingable: true,
|
||||||
LastHandshake: &recent,
|
LastHandshake: &recent,
|
||||||
},
|
},
|
||||||
want: true,
|
timeout: defaultTimeout,
|
||||||
|
want: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Not pingable but recent handshake",
|
name: "Not pingable but recent handshake",
|
||||||
@@ -29,7 +35,8 @@ func TestPeerStatus_IsConnected(t *testing.T) {
|
|||||||
IsPingable: false,
|
IsPingable: false,
|
||||||
LastHandshake: &recent,
|
LastHandshake: &recent,
|
||||||
},
|
},
|
||||||
want: true,
|
timeout: defaultTimeout,
|
||||||
|
want: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Pingable but old handshake",
|
name: "Pingable but old handshake",
|
||||||
@@ -37,15 +44,44 @@ func TestPeerStatus_IsConnected(t *testing.T) {
|
|||||||
IsPingable: true,
|
IsPingable: true,
|
||||||
LastHandshake: &past,
|
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{
|
status: PeerStatus{
|
||||||
IsPingable: false,
|
IsPingable: false,
|
||||||
LastHandshake: &past,
|
LastHandshake: &past,
|
||||||
},
|
},
|
||||||
want: false,
|
timeout: defaultTimeout,
|
||||||
|
want: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Pingable and no handshake",
|
name: "Pingable and no handshake",
|
||||||
@@ -53,7 +89,8 @@ func TestPeerStatus_IsConnected(t *testing.T) {
|
|||||||
IsPingable: true,
|
IsPingable: true,
|
||||||
LastHandshake: nil,
|
LastHandshake: nil,
|
||||||
},
|
},
|
||||||
want: true,
|
timeout: defaultTimeout,
|
||||||
|
want: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Not pingable and no handshake",
|
name: "Not pingable and no handshake",
|
||||||
@@ -61,12 +98,13 @@ func TestPeerStatus_IsConnected(t *testing.T) {
|
|||||||
IsPingable: false,
|
IsPingable: false,
|
||||||
LastHandshake: nil,
|
LastHandshake: nil,
|
||||||
},
|
},
|
||||||
want: false,
|
timeout: defaultTimeout,
|
||||||
|
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