Compare commits

..

26 Commits

Author SHA1 Message Date
Christoph
95394628d3 Merge branch 'master' into stable 2026-03-03 17:02:52 +01:00
Christoph Haas
b553375c43 Merge branch 'master' into stable 2026-02-28 22:32:43 +01:00
htiryaki-oe24
0a8ec71b3f Fixes minor chart issues #629 (#630)
(cherry picked from commit 3e0ffec07c)
2026-02-24 22:41:28 +01:00
h44z
fe4485037a Merge commit from fork 2026-02-24 22:40:13 +01:00
Arnaud Rocher
6e47d8c3e9 fix: parity of Base64/URL encoding between frontend and backend (#611)
Signed-off-by: Arnaud Rocher <arnaud.roche3@gmail.com>
(cherry picked from commit 5d58df8a19)
2026-01-29 22:39:33 +01:00
h44z
eb28492539 Merge commit from fork
* fix: prevent open redirect in OAuth return URL validation

* reformat check

---------

Co-authored-by: Arne Cools <arne.cools@intigriti.com>
(cherry picked from commit e62db0d62e)
2026-01-29 22:38:42 +01:00
Christoph
d1a4ddde10 Merge branch 'master' into stable 2025-11-23 21:00:12 +01:00
Christoph Haas
b1637b0c4e Merge branch 'master' into stable
# Conflicts:
#	internal/domain/peer.go
2025-10-19 13:25:07 +02:00
h44z
0cc7ebb83e ensure hooks run after restart (#494) (#497)
(cherry picked from commit 99df4ca3cd)
2025-09-03 22:48:45 +02:00
h44z
eb6a787cfc ensure that LDAP filter values are escaped (#512)
(cherry picked from commit 0cbca61c15)
2025-09-03 22:47:40 +02:00
Christoph Haas
b546eec4ed fix multi-peer generation, fix prefix handling (#491)
(cherry picked from commit c20f17cddf)
2025-08-12 21:25:48 +02:00
h44z
9be2133220 fix migration tool (#495) (#496)
(cherry picked from commit 9884d8c002)
2025-08-12 21:23:30 +02:00
Christoph Haas
b05837b2d9 ensure that v2 (or just 2) tags are only published for stable releases (#493)
(cherry picked from commit b099e8abfa)
2025-08-12 21:23:28 +02:00
Christoph Haas
08c8f8eac0 backport username display bugfix (#456) 2025-06-12 19:11:25 +02:00
Christoph Haas
d864e24145 improve logging of OAuth login issues, decrease auth-code exchange timeout (#451)
(cherry picked from commit e3b65ca337)
2025-06-12 19:07:46 +02:00
Christoph Haas
5b56e58fe9 fix self-provisioned peer-generation (#452)
(cherry picked from commit 61d8aa6589)
2025-06-09 17:41:29 +02:00
Christoph Haas
930ef7b573 Merge branch 'master' into stable 2025-05-16 09:58:14 +02:00
Christoph Haas
18296673d7 Merge branch 'master' into stable 2025-05-13 20:25:27 +02:00
Christoph Haas
4ccc59c109 Merge branch 'master' into stable
# Conflicts:
#	.github/workflows/docker-publish.yml
#	README.md
#	assets/tpl/admin_index.html
#	assets/tpl/user_index.html
#	cmd/wg-portal/main.go
#	docker-compose.yml
#	go.mod
#	go.sum
#	internal/common/util.go
#	internal/server/docs/docs.go
#	internal/server/handlers_common.go
#	internal/server/server.go
#	internal/wireguard/peermanager.go
2025-05-04 20:38:55 +02:00
onyx-flame
e6b01a9903 Feature (v1): add latest handshake data to API response (#203)
* feature: updated handshake-related fields type

* feature: updated handshake representations in templates

* feature: added handshake field to Swagger schema
2023-12-23 12:56:52 +01:00
Christoph Haas
2f79dd04c0 adopt github actions 2023-10-26 11:29:34 +02:00
Christoph Haas
e5ed9736b3 update docker build settings, move to new docker hub repository, use stable branch and major version tags 2023-10-26 11:22:58 +02:00
Christoph Haas
c8353b85ae Merge branch 'replace_ext_lib' into stable 2023-10-26 10:40:06 +02:00
Christoph Haas
6142031387 update gin 2023-10-26 10:39:01 +02:00
Christoph Haas
dd86d0ff49 replace inaccessible external lib 2023-10-26 10:31:29 +02:00
Christoph Haas
bdd426a679 populate peer device type (#170) 2023-10-26 10:20:08 +02:00
8 changed files with 22 additions and 87 deletions

View File

@@ -28,7 +28,6 @@ core:
backend:
default: local
rekey_timeout_interval: 125s
local_resolvconf_prefix: tun.
advanced:
@@ -204,13 +203,6 @@ 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`

View File

@@ -35,14 +35,6 @@ 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.
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**:
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.

View File

@@ -204,13 +204,13 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
// calculate if session was restarted
p.UpdatedAt = now
p.LastSessionStart = c.getSessionStartTime(*p, peer.BytesUpload, peer.BytesDownload,
p.LastSessionStart = 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(c.cfg.Backend.ReKeyTimeoutInterval)
p.CalcConnected()
if wasConnected != p.IsConnected {
slog.Debug("peer connection state changed",
@@ -249,7 +249,7 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
}
}
func (c *StatisticsCollector) getSessionStartTime(
func getSessionStartTime(
oldStats domain.PeerStatus,
newReceived, newTransmitted uint64,
latestHandshake *time.Time,
@@ -258,7 +258,7 @@ func (c *StatisticsCollector) getSessionStartTime(
return nil // currently not 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
oldestHandshakeTime := time.Now().Add(-2 * time.Minute) // if a handshake is older than 2 minutes, 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(c.cfg.Backend.ReKeyTimeoutInterval)
p.CalcConnected()
if wasConnected != p.IsConnected {
connectionStateChanged = true

View File

@@ -5,11 +5,10 @@ import (
"testing"
"time"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
)
func TestStatisticsCollector_getSessionStartTime(t *testing.T) {
func Test_getSessionStartTime(t *testing.T) {
now := time.Now()
nowMinus1 := now.Add(-1 * time.Minute)
nowMinus3 := now.Add(-3 * time.Minute)
@@ -134,14 +133,7 @@ func TestStatisticsCollector_getSessionStartTime(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
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,
if got := 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)
}

View File

@@ -10,8 +10,6 @@ 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")
@@ -117,8 +115,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)

View File

@@ -139,7 +139,6 @@ 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.

View File

@@ -21,8 +21,8 @@ type PeerStatus struct {
LastSessionStart *time.Time `gorm:"column:last_session_start" json:"LastSessionStart"`
}
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
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
handshakeValid := false
if s.LastHandshake != nil {

View File

@@ -9,16 +9,11 @@ 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
timeout time.Duration
want bool
name string
status PeerStatus
want bool
}{
{
name: "Pingable and recent handshake",
@@ -26,8 +21,7 @@ func TestPeerStatus_IsConnected(t *testing.T) {
IsPingable: true,
LastHandshake: &recent,
},
timeout: defaultTimeout,
want: true,
want: true,
},
{
name: "Not pingable but recent handshake",
@@ -35,8 +29,7 @@ func TestPeerStatus_IsConnected(t *testing.T) {
IsPingable: false,
LastHandshake: &recent,
},
timeout: defaultTimeout,
want: true,
want: true,
},
{
name: "Pingable but old handshake",
@@ -44,44 +37,15 @@ func TestPeerStatus_IsConnected(t *testing.T) {
IsPingable: true,
LastHandshake: &past,
},
timeout: defaultTimeout,
want: true,
want: true,
},
{
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)",
name: "Not pingable and old handshake",
status: PeerStatus{
IsPingable: false,
LastHandshake: &past,
},
timeout: defaultTimeout,
want: false,
want: false,
},
{
name: "Pingable and no handshake",
@@ -89,8 +53,7 @@ func TestPeerStatus_IsConnected(t *testing.T) {
IsPingable: true,
LastHandshake: nil,
},
timeout: defaultTimeout,
want: true,
want: true,
},
{
name: "Not pingable and no handshake",
@@ -98,13 +61,12 @@ func TestPeerStatus_IsConnected(t *testing.T) {
IsPingable: false,
LastHandshake: nil,
},
timeout: defaultTimeout,
want: false,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.status.CalcConnected(tt.timeout)
tt.status.CalcConnected()
if got := tt.status.IsConnected; got != tt.want {
t.Errorf("IsConnected = %v, want %v", got, tt.want)
}