Client disconnected a few seconds after authentication

Hello,

When some of our users (different geographical locations and ISP) try to connect to their usual Pritunl profile, they pass the Okta SSO authentications successfully, but are disconnected from the VPN immediately after. The same behavior happens every time they retry.
Other users of the same profile are totally fine and we couldn’t find the difference between these two groups yet.

Here are the service logs from an impacted user:

[2025-05-14 11:10:52][INFO] ▶ profile: Connecting ◆ device_auth=false ◆ disable_dns=false ◆ disable_gateway=false ◆ dynamic_firewall=false ◆ force_connect=false ◆ force_dns=false ◆ geo_sort="" ◆ mode="ovpn" ◆ profile_id="88d869d56e07b09d" ◆ reconnect=false ◆ sso_auth=true
[2025-05-14 11:10:52][INFO] ▶ connection: Resolved remotes ◆ public_address="" ◆ public_address6="" ◆ remotes=[]string{"x.x.x.x"} ◆ sort_method="random"
[2025-05-14 11:10:52][INFO] ▶ connection: Attempting remote ◆ client_disconnect=false ◆ client_disconnect_waiters=0 ◆ client_disconnected=false ◆ client_provider=true ◆ client_startime=0 ◆ data_iface="" ◆ data_mode="" ◆ data_remotes=[]string{"x.x.x.x"} ◆ data_status="connecting" ◆ data_timestamp=0 ◆ data_tun_iface="" ◆ ovpn_auth_failed=false ◆ ovpn_cmd=false ◆ ovpn_connected=false ◆ ovpn_dir="" ◆ ovpn_last_auth_failed=-1 ◆ ovpn_management_pass=false ◆ ovpn_management_port=0 ◆ ovpn_path="/Applications/Pritunl.app/Contents/Resources/pritunl-openvpn" ◆ ovpn_remotes=[]string{} ◆ ovpn_running=0 ◆ ovpn_tap_iface="" ◆ profile_device_auth=false ◆ profile_disable_dns=false ◆ profile_disable_gateway=false ◆ profile_dynamic_firewall=false ◆ profile_force_connect=false ◆ profile_force_dns=false ◆ profile_geo_sort=false ◆ profile_id="88d869d56e07b09d" ◆ profile_mode="ovpn" ◆ profile_reconnect=false ◆ profile_sso_auth=true ◆ profile_system_profile=false ◆ profile_timeout=false ◆ remote="x.x.x.x" ◆ state_closed=false ◆ state_closed_waiters=0 ◆ state_deadline=false ◆ state_delay=false ◆ state_id="3e5941cb2c042b00" ◆ state_interactive=true ◆ state_no_reconnect=false ◆ state_stop=false ◆ state_system_interactive=false ◆ state_temp_paths=[]string{} ◆ state_time=time.Date(2025, time.May, 14, 11, 10, 52, 142402000, time.Local) ◆ wg_bash_path="/Applications/Pritunl.app/Contents/Resources/bash" ◆ wg_conf_path="" ◆ wg_conf_path2="" ◆ wg_connected=false ◆ wg_last_handshake=0 ◆ wg_path="/Applications/Pritunl.app/Contents/Resources/wg" ◆ wg_priv_key=false ◆ wg_pub_key=false ◆ wg_quick_path="/Applications/Pritunl.app/Contents/Resources/wg-quick" ◆ wg_server_pub_key=false ◆ wg_sso_start=time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC) ◆ wg_sso_token=false ◆ wg_util_path=""
[2025-05-14 11:10:52][ERRO] ▶ profile: All connection requests failed
profile: Request put error
Post "https://vpn.example.com/key/ovpn/65ef0e606c0ffacdf167ffb0/661e760d6c0ffacdf183bd5c/660c0b6f6c0ffacdf178b3f2": read tcp 192.168.1.110:59088->x.x.x.x:443: read: connection reset by peer
ORIGINAL STACK TRACE:
github.com/pritunl/pritunl-client-electron/service/connection.(*Client).EncRequest
    /Users/apple/go/src/github.com/pritunl/pritunl-client-electron/service/connection/client.go:987 +0x1054433f0
github.com/pritunl/pritunl-client-electron/service/connection.(*Client).authorize
    /Users/apple/go/src/github.com/pritunl/pritunl-client-electron/service/connection/client.go:545 +0x105440907
github.com/pritunl/pritunl-client-electron/service/connection.(*Client).connectPreAuth
    /Users/apple/go/src/github.com/pritunl/pritunl-client-electron/service/connection/client.go:287 +0x10543f167
github.com/pritunl/pritunl-client-electron/service/connection.(*Client).Start
    /Users/apple/go/src/github.com/pritunl/pritunl-client-electron/service/connection/client.go:189 +0x10543e39f
github.com/pritunl/pritunl-client-electron/service/connection.(*Ovpn).Start
    /Users/apple/go/src/github.com/pritunl/pritunl-client-electron/service/connection/ovpn.go:107 +0x10544571f
github.com/pritunl/pritunl-client-electron/service/connection.(*Connection).Start
    /Users/apple/go/src/github.com/pritunl/pritunl-client-electron/service/connection/connection.go:127 +0x105445708
github.com/pritunl/pritunl-client-electron/service/handlers.profilePost.func1
    /Users/apple/go/src/github.com/pritunl/pritunl-client-electron/service/handlers/profile.go:148 +0x10546e38b
runtime.goexit
    /opt/homebrew/Cellar/go@1.23/1.23.7/libexec/src/runtime/asm_arm64.s:1223 +0x104ecf363

There are no logs on the Pritunl server side.
A tcpdump captured on the Pritunl server seems to show the TLS handshake ending with a TCP RST. There is no traffic between the server and the client afterwards.

The Pritunl client version being used is v1.3.4262.38
The Pritunl server is running v1.32.4258.38 (e8414a) on Ubuntu 22.04.5
Most clients are running on MacOS 15.x versions

We tried lowering the “Connection MTU” to 1200 on the server, but it didn’t helped.
We also tried to recreate our VPN server on AlmaLinux 9.5 to benefit from more recent packages, but the same users are facing the same issue on this server too.

The Pritunl server had not been modified for some time when the first user started reporting this issue, around two weeks ago.

Do you know what could be causing this?

Best regards,
Julien

It’s failing on the pre-connection authentication which is done with a web request to the Pritunl web server port. It wouldn’t be effected by MTU or any settings with the VPN. Check for errors in the server and verify the CPU or memory are not overloaded. Also run sudo journalctl -u pritunl -n 5000 and look for errors coming directly from the web server process.

Running sudo pritunl set app.web_systemd true and sudo systemctl restart pritunl may help if there are resource issues on the server. Then run sudo journalctl -u pritunl-web -n 5000 and check for errors from the web server.

When an impacted user tries to connect there is no log showing for pritunl or pritunl-web. We tested that with app.web_systemd to false and true and there is no difference.

I can confirm that the server isn’t overloaded.

We also noticed that if impacted people connect to another VPN service first and then connect to their Pritunl profile the connection is established successfully.

A bit more details about what we saw from running a local build. I hope it helps.

First, for a user not facing the issue.
We see the bellow multiple times:

GET PROFILE URL:  http://unix/profile/4bxpncedtjve3ju0

AUTHKEY: ca5ohWos2yaedoh1Aeseisee0oJeish7shoh7Looquieghohxe5areW1ieK1eaSh

REQUEST: &{Method:GET URL:http://unix/profile/4bxpncedtjve3ju0 Proto:HTTP/1.1 ProtoMajor:1 ProtoMinor:1 Header:map[Auth-Key:[ca5ohWos2yaedoh1Aeseisee0oJeish7shoh7Looquieghohxe5areW1ieK1eaSh] Content-Type:[application/json] User-Agent:[pritunl]] Body:<nil> GetBody:<nil> ContentLength:0 TransferEncoding:[] Close:false Host:unix Form:map[] PostForm:map[] MultipartForm:<nil> Trailer:map[] RemoteAddr: RequestURI: TLS:<nil> Cancel:<nil> Response:<nil> Pattern: ctx:{emptyCtx:{}} pat:<nil> matches:[] otherValues:map[]}

RESPONSE: &{Status:200 OK StatusCode:200 Proto:HTTP/1.1 ProtoMajor:1 ProtoMinor:1 Header:map[Content-Length:[582] Content-Type:[application/json; charset=utf-8] Date:[Thu, 15 May 2025 14:17:18 GMT]] Body:0x14000197220 ContentLength:582 TransferEncoding:[] Close:false Uncompressed:false Trailer:map[] Request:0x1400011d7c0 TLS:<nil>}

PROFILE: PROFILE: <nil>

Then it changes to (note that profile is not nil anymore) this, which repeats a few times too:

GET PROFILE URL:  http://unix/profile/4bxpncedtjve3ju0

AUTHKEY: ca5ohWos2yaedoh1Aeseisee0oJeish7shoh7Looquieghohxe5areW1ieK1eaSh

REQUEST: &{Method:GET URL:http://unix/profile/4bxpncedtjve3ju0 Proto:HTTP/1.1 ProtoMajor:1 ProtoMinor:1 Header:map[Auth-Key:[ca5ohWos2yaedoh1Aeseisee0oJeish7shoh7Looquieghohxe5areW1ieK1eaSh] Content-Type:[application/json] User-Agent:[pritunl]] Body:<nil> GetBody:<nil> ContentLength:0 TransferEncoding:[] Close:false Host:unix Form:map[] PostForm:map[] MultipartForm:<nil> Trailer:map[] RemoteAddr: RequestURI: TLS:<nil> Cancel:<nil> Response:<nil> Pattern: ctx:{emptyCtx:{}} pat:<nil> matches:[] otherValues:map[]}

RESPONSE: &{Status:200 OK StatusCode:200 Proto:HTTP/1.1 ProtoMajor:1 ProtoMinor:1 Header:map[Content-Length:[582] Content-Type:[application/json; charset=utf-8] Date:[Thu, 15 May 2025 14:17:18 GMT]] Body:0x14000197260 ContentLength:582 TransferEncoding:[] Close:false Uncompressed:false Trailer:map[] Request:0x1400011db80 TLS:<nil>}

PROFILE: &{Id:4bxpncedtjve3ju0 Mode: Iface: Tuniface: Routes:[] Routes6:[] Reconnect:false Status:connecting Timestamp:0 GatewayAddr: GatewayAddr6: ServerAddr: ClientAddr: MacAddr:c8:19:06:7d:39:ea MacAddrs:[] SsoUrl:}

Finally, the Okta URL is displayed:

GET PROFILE URL:  http://unix/profile/4bxpncedtjve3ju0

AUTHKEY: ca5ohWos2yaedoh1Aeseisee0oJeish7shoh7Looquieghohxe5areW1ieK1eaSh

REQUEST: &{Method:GET URL:http://unix/profile/4bxpncedtjve3ju0 Proto:HTTP/1.1 ProtoMajor:1 ProtoMinor:1 Header:map[Auth-Key:[ca5ohWos2yaedoh1Aeseisee0oJeish7shoh7Looquieghohxe5areW1ieK1eaSh] Content-Type:[application/json] User-Agent:[pritunl]] Body:<nil> GetBody:<nil> ContentLength:0 TransferEncoding:[] Close:false Host:unix Form:map[] PostForm:map[] MultipartForm:<nil> Trailer:map[] RemoteAddr: RequestURI: TLS:<nil> Cancel:<nil> Response:<nil> Pattern: ctx:{emptyCtx:{}} pat:<nil> matches:[] otherValues:map[]}

RESPONSE: &{Status:200 OK StatusCode:200 Proto:HTTP/1.1 ProtoMajor:1 ProtoMinor:1 Header:map[Content-Length:[710] Content-Type:[application/json; charset=utf-8] Date:[Thu, 15 May 2025 14:17:18 GMT]] Body:0x14000108800 ContentLength:710 TransferEncoding:[] Close:false Uncompressed:false Trailer:map[] Request:0x140003192c0 TLS:<nil>}

PROFILE: &{Id:4bxpncedtjve3ju0 Mode: Iface: Tuniface: Routes:[] Routes6:[] Reconnect:false Status:authenticating Timestamp:0 GatewayAddr: GatewayAddr6: ServerAddr: ClientAddr: MacAddr:c8:19:06:7d:39:ea MacAddrs:[] SsoUrl:https://vpn.example.com/key/request?state=OqMiABVbXUu0J24mGsJP4luLUa8Jwmdj1KmH27GGCMPRZ4Uy2tSMayNmyAbYqo8P}

Single sign-on authentication required, open link to complete authentication:
https://vpn.example.com/key/request?state=OqMiABVbXUu0J24mGsJP4luLUa8Jwmdj1KmH27GGCMPRZ4Uy2tSMayNmyAbYqo8P

Now, for a user facing the problem, instead of getting an Okta URL, the struct suddenly changes from connecting to disconnecting:

&{Id:hsiotfqoj3psvyyg Mode: Iface: Tuniface: Routes:[] Routes6:[] Reconnect:false Status:disconnecting Timestamp:0 GatewayAddr: GatewayAddr6: ServerAddr: ClientAddr: MacAddr:9b:8a:ca:a5:4f:22 MacAddrs:[] SsoUrl:}

The repetition of queries before the failure/success seems to be caused by a 404 coming from: pritunl-client-electron/cli/profile/utils.go at 1.3.4261.88 · pritunl/pritunl-client-electron · GitHub
Probably because this seems to return nil pritunl-client-electron/service/connection/store.go at 1.3.4261.88 · pritunl/pritunl-client-electron · GitHub

In that code after a connection that requires single sign-on is sent to the background service the CLI client will immediately begins a loop to get the single sign-on authorization URL from the background service. It will check every 100ms until it get’s the URL. That shouldn’t cause any issues and it’s also only done with the CLI. The GUI application has an event stream that is used to notify the GUI.

There was a possible context leak in the code where the connection reset error is occurring. Fixed in pritunl-client-electron:56e1994. I had left a TODO message about it but never fixed it. But I don’t think it would produce that error and the context leak is likely not possible with the current usage of the code. I did try to reproduce any possibly race conditions and it still wouldn’t return connection reset by peer.

Also run the lsof commands listed in the server debugging documentation. Check for excessive threads or open connections. Do this shortly after the issue occurs.

There may be an issue with the web server thread adjustments made about 6 months ago. First run sudo pritunl get app.request_queue_size if this returns 50 it is the newer release with the higher values. If it is revert it to the previous values by running the commands below then restart the Pritunl service. If it isn’t 50 you should try updating the server because there was a resource issue specifically with WireGuard connections which were overloading the server due to excessive RSA validation of every WireGuard ping request.

sudo pritunl set app.request_queue_size 10
sudo pritunl set app.request_accepted_queue_size 50
sudo pritunl set app.request_thread_count 250
sudo pritunl set app.request_max_thread_count 50

sudo systemctl restart pritunl

Another option is to install py-spy as documented in the server debugging process inspection to look for slow running functions.

Thank you for looking into this.

Running the lsof commands and monitoring the number of open files during the connection attempts didn’t showed any noticeable change. We ran these checks before and after changing the app.request_* values you mentioned (swapping the values for the last two settings).

We also ran py-spy and didn’t saw anything special there.

Just in case, we also scaled up our Pritunl instance to c7a.large host in AWS. The machine is mostly idle.

In order to mitigate the problem for our users we cloned our VPN server into another temporary one, on the same host, where the Single Sign-On Authentication parameter is disabled.
The users reporting the issues are the only one authorized to download the profile using this temporary server. A few more people reported the problem today, even if they didn’t changed anything voluntarily since last week, when the usual profile was still working normally for them.

We believe there might be some local update impacting their Pritunl client behavior somehow, when the SSO auth is enabled. We’re still trying to figure out what exactly since no one in my team can reproduce the issue.

I will look into reproducing the issue. The difference between a single sign-on connection and the other connections is the web request before the VPN connection. The other connections will directly connect to the VPN server. With single sign-on and several other authentication modes a web request to /key/ovpn is sent first to complete the authentication which then provides a token. This token is then used for the OpenVPN connection. WireGuard always uses authentication with web requests.

You should verify there is nothing interfering with that web server. Try running the command below in a PowerShell terminal to simulate a POST with JSON data that is similar to the client request. This should return a 401 error with auth_invalid. If there is a load balancer or any kind of web application firewall those may filter requests that don’t appear to come from a web browser.

Windows PowerShell code

$uri = "https://<server_ip>/auth/session"
$body = @{ username = "test"; password = "test" } | ConvertTo-Json
$response = Invoke-RestMethod -Uri $uri -Method Post -Body $body -ContentType "application/json"
$response

Linux curl

curl -X POST https://<server_ip>/auth/session \
  -H "Content-Type: application/json" \
  -H "User-Agent: pritunl-client" \
  -d '{"username":"test", "password":"test"}'

I can confirm that nothing interferes with the web server. It is running on a publicly available host without any load balancer or anything else in front of it.

I asked several users to run the curl command you provided and they all got the same output:

{"error": "auth_invalid", "error_msg": "Authentication credentials are not valid."}%