/*
* Copyright (c) 2025 Proton AG
*
* This file is part of ProtonVPN.
*
* ProtonVPN is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonVPN is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonVPN. If not, see .
*/
using System;
using System.Net;
using Newtonsoft.Json;
using ProtonVPN.Common.Core.Networking;
using ProtonVPN.Common.Legacy.Go;
using ProtonVPN.Common.Legacy.Vpn;
using ProtonVPN.Logging.Contracts;
using ProtonVPN.Logging.Contracts.Events.ConnectionLogs;
using ProtonVPN.Logging.Contracts.Events.LocalAgentLogs;
using ProtonVPN.Vpn.Config;
using ProtonVPN.Vpn.Connection;
using ProtonVPN.Vpn.Gateways;
using ProtonVPN.Vpn.LocalAgent.Contracts;
namespace ProtonVPN.Vpn.LocalAgent;
internal class LocalAgent : ILocalAgent
{
private const int MINIMUM_NETSHIELD_STATS_TIMEOUT_IN_SECONDS = 20;
private const int DEFAULT_PORT = 65432;
private readonly ILogger _logger;
private readonly IGatewayCache _gatewayCache;
public VpnError LastError { get; private set; }
private VpnConfig? _vpnConfig;
private bool _isTlsChannelActive;
private DateTime _lastNetShieldStatsRequestDate = DateTime.MinValue;
public LocalAgent(
ILogger logger,
IGatewayCache gatewayCache)
{
_logger = logger;
_gatewayCache = gatewayCache;
}
public bool ConnectToTlsChannel(LocalAgentConnectParams localAgentConnectParams)
{
IPAddress? gatewayIPAddress = _gatewayCache.Get();
if (gatewayIPAddress == null)
{
_logger.Error("Default gateway is missing. Disconnecting.");
LastError = VpnError.Unknown;
return false;
}
using GoString clientCertPem = localAgentConnectParams.ClientCertPem.ToGoString();
using GoString clientKeyPem = localAgentConnectParams.ClientSecretPem.ToGoString();
using GoString serverCaPem = VpnCertConfig.ROOT_CA.ToGoString();
using GoString host = $"{gatewayIPAddress}:{DEFAULT_PORT}".ToGoString();
using GoString featuresJson = GetFeatures(localAgentConnectParams).ToGoString();
using GoString certServerName = localAgentConnectParams.Server.Name.ToGoString();
string result = PInvoke.Connect(
clientCertPem,
clientKeyPem,
serverCaPem,
host,
certServerName,
featuresJson,
connectivity: true,
keepAliveSeconds: 60,
// Zero falls back to the default value of 9
keepAliveMaxCount: 0).ConvertToString();
if (result == "")
{
_logger.Info("Channel opened.");
}
else
{
_logger.Error("Failed to connect to TLS channel: " + result);
LastError = GetVpnError(result);
return false;
}
_isTlsChannelActive = true;
return true;
}
public void CloseTlsChannel()
{
if (_isTlsChannelActive)
{
_isTlsChannelActive = false;
PInvoke.Close();
_logger.Info("Channel closed.");
}
}
private static string GetFeatures(LocalAgentConnectParams localAgentConnectParams)
{
return GetFeaturesJson(new FeaturesContract
{
Bouncing = localAgentConnectParams.Server.Label,
SplitTcp = localAgentConnectParams.VpnConfig.SplitTcp,
NetShieldLevel = localAgentConnectParams.VpnConfig.NetShieldMode,
PortForwarding = localAgentConnectParams.VpnConfig.PortForwarding,
RandomizedNat = !localAgentConnectParams.VpnConfig.ModerateNat,
});
}
private static VpnError GetVpnError(string result)
{
return result.Contains("private key does not match public key")
? VpnError.ClientKeyMismatch
: VpnError.Unknown;
}
public void SetFeatures(VpnFeatures vpnFeatures)
{
if (!_isTlsChannelActive)
{
return;
}
UpdateVpnConfig(vpnFeatures);
using GoString goFeatures = GetFeatures(vpnFeatures).ToGoString();
PInvoke.SetFeatures(goFeatures);
}
private void UpdateVpnConfig(VpnFeatures vpnFeatures)
{
if (_vpnConfig == null)
{
return;
}
_vpnConfig = new VpnConfig(new()
{
Ports = _vpnConfig.Ports,
CustomDns = _vpnConfig.CustomDns,
SplitTunnelMode = _vpnConfig.SplitTunnelMode,
SplitTunnelIPs = _vpnConfig.SplitTunnelIPs,
OpenVpnAdapter = _vpnConfig.OpenVpnAdapter,
VpnProtocol = _vpnConfig.VpnProtocol,
PreferredProtocols = _vpnConfig.PreferredProtocols,
NetShieldMode = vpnFeatures.NetShieldMode,
SplitTcp = vpnFeatures.SplitTcp,
PortForwarding = vpnFeatures.PortForwarding,
IsIpv6Enabled = _vpnConfig.IsIpv6Enabled,
WireGuardConnectionTimeout = _vpnConfig.WireGuardConnectionTimeout,
DnsBlockMode = _vpnConfig.DnsBlockMode,
});
}
public void RequestNetShieldStats()
{
if (_lastNetShieldStatsRequestDate.AddSeconds(MINIMUM_NETSHIELD_STATS_TIMEOUT_IN_SECONDS) < DateTime.UtcNow
&& _isTlsChannelActive)
{
_lastNetShieldStatsRequestDate = DateTime.UtcNow;
PInvoke.SendGetStatus(true);
}
}
private static string GetFeatures(VpnFeatures vpnFeatures)
{
return GetFeaturesJson(new FeaturesContract
{
SplitTcp = vpnFeatures.SplitTcp,
NetShieldLevel = vpnFeatures.NetShieldMode,
PortForwarding = vpnFeatures.PortForwarding,
RandomizedNat = !vpnFeatures.ModerateNat,
});
}
private static string GetFeaturesJson(FeaturesContract contract)
{
return JsonConvert.SerializeObject(contract, Formatting.None, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
});
}
}