C#检测公网IP变更异常:VPN开启后无法识别IP变化问题
公网IP变更检测异常问题分析
问题背景
使用以下C#代码跟踪桌面应用的公网IP变更,可检测公网IP本身变化或VPN切换IP的情况,代码在应用启动及需要检查时执行:
public class PublicIP { IPAddress last_ip=null; DateTime timestamp_lastipchange; public void UpdateIP() { List<string> hosts = new List<string>() { "https://api.ipify.org", "https://ipinfo.io/ip", "https://checkip.amazonaws.com", "https://wtfismyip.com/text", "http://icanhazip.com" }; using(WebClient webclient = new WebClient()) { foreach(string host in hosts) { //Download each string from hosts until an IP could be fetched try{ var newip = IPAddress.Parse(webclient.DownloadString(service)); //Downloading the string if(!newip.IsEqual(last_ip) && last_ip!=null) timestamp_lastipchange = DateTime.Now; //Check if the ip changed, if the last known ip does not exists skipp this step last_ip = newip; //Save last known ip return; } catch { } } } } }
测试异常场景
多数场景运行正常,但单元测试发现以下场景无法检测IP变化:
- 切换网络导致IP变化:可成功检测
- 运营商变更IP:可成功检测
- 应用启动时VPN已开启,随后关闭:可成功检测
- 应用启动时VPN关闭,运行中开启VPN:无法检测到变化,
WebClient.DownloadString()仍返回VPN开启前的IP
疑问
该场景下无法检测IP变化的原因是什么?是否需要手动选择VPN网络接口,或是存在客户端/服务端缓存问题?
原因分析
1. 客户端连接池与路由缓存问题
当应用启动时VPN未开启,系统会为IP检测服务解析DNS并建立TCP连接。开启VPN后,系统路由表更新,但WebClient基于的HttpWebRequest默认启用连接池复用机制,已建立的旧连接不会自动断开,后续请求仍通过旧连接(非VPN路由)发送,因此返回的还是VPN开启前的IP。
另外,系统DNS缓存也可能导致服务域名解析结果未更新,但核心问题还是连接复用。
2. 服务端缓存排除
IP检测服务都是实时返回当前请求的源IP,不会缓存单个客户端的IP信息,所以服务端缓存不是问题。
3. 无需手动选择VPN网络接口
手动指定网络接口操作复杂且不通用,不同VPN的接口标识差异大,不是可行的解决方案。
解决方案
针对连接复用问题,可通过以下方式修正:
方案1:禁用WebClient连接复用
在请求头中添加Connection: Close,强制每次请求后关闭连接,避免复用旧连接:
public void UpdateIP() { List<string> hosts = new List<string>() { "https://api.ipify.org", "https://ipinfo.io/ip", "https://checkip.amazonaws.com", "https://wtfismyip.com/text", "http://icanhazip.com" }; using(WebClient webclient = new WebClient()) { // 添加Close头禁用连接复用 webclient.Headers.Add(HttpRequestHeader.Connection, "Close"); foreach(string host in hosts) { try{ // 修正原代码变量错误:service改为host var newip = IPAddress.Parse(webclient.DownloadString(host)); if(last_ip != null && !newip.Equals(last_ip)) timestamp_lastipchange = DateTime.Now; last_ip = newip; return; } catch { } } } }
方案2:替换为HttpClient(推荐)
HttpClient支持更灵活的连接池配置,可设置连接生命周期限制,避免长期复用旧连接:
// 建议全局单例HttpClient,避免频繁创建 private static readonly HttpClient _httpClient = new HttpClient(new HttpClientHandler { // 设置连接存活时间,到期自动回收 PooledConnectionLifetime = TimeSpan.FromMinutes(1) }); public async Task UpdateIPAsync() { List<string> hosts = new List<string>() { "https://api.ipify.org", "https://ipinfo.io/ip", "https://checkip.amazonaws.com", "https://wtfismyip.com/text", "http://icanhazip.com" }; foreach(string host in hosts) { try{ var ipStr = await _httpClient.GetStringAsync(host); var newip = IPAddress.Parse(ipStr.Trim()); if(last_ip != null && !newip.Equals(last_ip)) timestamp_lastipchange = DateTime.Now; last_ip = newip; return; } catch { } } }
内容的提问来源于stack exchange,提问作者mrpigus
相关产品推荐
相关产品推荐

