服务器自动更新

This commit is contained in:
snltty
2024-07-19 15:57:16 +08:00
parent b16ffa3ea4
commit 7a039d4b1c
37 changed files with 760 additions and 267 deletions

View File

@@ -38,3 +38,10 @@
请作者喝一杯咖啡,使其更有精力更新代码
<p><img src="./readme/qr.jpg" width="360"></p>
</div>
## 感谢支持
<a href="https://mi-d.cn" target="_blank">
<img src="https://mi-d.cn/wp-content/uploads/2021/12/cropped-1639494965-网站LOGO无字.png" width="40" style="vertical-align: middle;"> 米多贝克</a>

View File

@@ -4,6 +4,8 @@ sidebar_position: 1
# 1、首页
## 1、说明
1. 使用 .NET8、所有通信功能均支持TCP+UDP
2. SSL通信加密打洞、中继、均支持ssl加密
3. 打洞内核**免费开源**,你可以使用**linker.tunnel**库将打洞集成到自己的项目中
@@ -12,4 +14,8 @@ sidebar_position: 1
6. 打洞失败回退、这需要你中继部署服务器进行中继通信
7. 少量的内存占用、易用的web管理页面、高性能的通信
<a href="https://jq.qq.com/?_wv=1027&k=ucoIVfz4" target="_blank">你可以加入QQ群1121552990</a>
<a href="https://jq.qq.com/?_wv=1027&k=ucoIVfz4" target="_blank">你可以加入QQ群1121552990 </a>
## 2、感谢支持
<a href="https://mi-d.cn" target="_blank"><img src="https://mi-d.cn/wp-content/uploads/2021/12/cropped-1639494965-网站LOGO无字.png" width="40" style={{verticalAlign: 'middle'}} /> 米多贝克</a>

View File

@@ -49,5 +49,9 @@ server.json
60000
]
},
//服务器更新密钥,客户端配置这个密钥,可以更新服务端
"Updater": {
"SecretKey": "46760C6B-5EA8-4FCB-B342-1D16A7CE9773"
},
}
```

View File

@@ -6,15 +6,16 @@ sidebar_position: 3
## 1、说明
1. **如果你自己部署了服务端,则可以修改服务器相关配置**
2. 如果你使用默认的 linker.snltty.com 服务器,你无需修改服务器相关配置,但是你无法使用**中继****服务器代理穿透** 功能
2. 如果你使用默认的 linker.snltty.com 服务器,你无需修改服务器相关配置,但是你无法使用**中继****服务器穿透**、**服务器更新** 功能,这些功能都需要服务端响应的密钥才能操作
## 2、看图
1. **信标服务器**客户端之间交换信息比如打洞需要交换外网IP和外网端口
2. **外网端口服务器**用于获取客户端外网IP和外网端口协助打洞
2. **端口服务器**用于获取客户端外网IP和外网端口协助打洞
3. **打洞协议**,有多种打洞协议,你可以调整顺序,优先使用喜欢的打洞协议进行打洞
4. **打洞排除IP**如果你希望有哪些IP不参与打洞你可以在这里配置比如 虚拟网卡 的IP一些VPN的IP
5. **中继加密秘钥**,当客户端与服务端秘钥不一致时,无法使用中继
6. **服务器代理穿透**,就是内网穿透,你需要填写服务端的密钥
5. **中继服务器**,用于在打洞失败后使用服务器中转进行通信,请填写你服务端的中继密钥,当客户端与服务端秘钥不一致时,无法使用中继
6. **服务器穿透**,就是内网穿透,你需要填写服务端的密钥
7. **服务器更新**,填写你服务器的更新密钥,让你的客户端有权自动更新服务器
![Docusaurus Plushie](./img/server.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 124 KiB

View File

@@ -4,9 +4,13 @@ sidebar_position: 3
# 3.3、虚拟网卡
1. 此一项,可以为每个设备安装一个**虚拟网卡**,然后分配一个**IP**通过IP地址访问设备。不受端口限制。
2. 两个设备之间通过**虚拟网卡**互相访问则两个设备都需要安装虚拟网卡且IP段一致比如都是 **192.168.54.x/24**,一个是**192.168.54.2**,一个是**192.168.54.3**,而不能配置为**192.168.54.2**和**192.168.55.3**
3. 如果你无法开启虚拟网卡,可以到<a target="_blank" href="https://github.com/xjasonlyu/tun2socks/releases/latest">tun2socks</a> 下载对应你系统的版本,覆盖`plugins/tuntap/` 下的 `tun2socks``tun2socks.exe`
## 1、配置虚拟网卡IP
在设备虚拟网卡一栏点击IP配置

View File

@@ -6,8 +6,8 @@ sidebar_position: 1
## 1、支持msquic
1. 在windowswin11支持msquic但是win10默认并不支持你可以删除**msquic.dll**,将**msquic-openssl.dll**更名为**msquic.dll**这或许能让win10支持msquic
2. linux,请按<a target="_blank" href="https://github.com/dotnet/runtime/tree/main/src/libraries/System.Net.Quic">官方说明</a>安装msquic
1. 在windows,如果提示不支持msquic可以到 <a target="_blank" href="https://github.com/microsoft/msquic/releases/latest">msquic</a> 下载合适你系统的版本覆盖根目录下的msquic.dll一般来说win10或以下版本需要openssl版本的msquic
2. linux请按<a target="_blank" href="https://github.com/dotnet/runtime/tree/main/src/libraries/System.Net.Quic">官方说明</a>安装msquic
## 2、打洞协议调整

View File

@@ -39,7 +39,15 @@ namespace linker.service
{
if (Process.GetProcessesByName(mainExeName).Any() == false)
{
RestartService();
if (File.Exists($"{mainExeName}.exe.temp"))
{
RestartService();
}
else
{
KillExe();
OpenExe();
}
}
}
catch (Exception)

View File

@@ -10,6 +10,8 @@ using System.Net.Sockets;
using System.Security.Authentication;
using System.Text;
using linker.tunnel.wanport;
using System.Runtime.InteropServices;
using System.IO;
namespace linker.tunnel.transport
{
@@ -729,31 +731,23 @@ namespace linker.tunnel.transport
}
private void TestQuic()
{
if (QuicListener.IsSupported == false)
if (OperatingSystem.IsWindows())
{
if (OperatingSystem.IsWindows())
if (QuicListener.IsSupported == false)
{
try
{
LoggerHelper.Instance.Info($"move msquic-openssl.dll -> msquic.dll");
File.Move("msquic-openssl.dll", "msquic.dll", true);
if (File.Exists("msquic-openssl.dll"))
{
LoggerHelper.Instance.Info($"copy msquic-openssl.dll -> msquic.dll");
File.Move("msquic-openssl.dll", "msquic.dll", true);
}
}
catch (Exception)
{
}
}
}
else
{
try
{
LoggerHelper.Instance.Info($"delete msquic-openssl.dll");
File.Delete("msquic-openssl.dll");
}
catch (Exception)
{
}
}
}

View File

@@ -9,4 +9,25 @@ export const confirm = (machineId, version) => {
}
export const exit = (machineId) => {
return sendWebsocketMsg('updaterclient/exit', machineId);
}
export const getSecretKey = () => {
return sendWebsocketMsg('updaterclient/GetSecretKey');
}
export const setSecretKey = (data) => {
return sendWebsocketMsg('updaterclient/SetSecretKey', data);
}
export const getUpdaterCurrent = () => {
return sendWebsocketMsg('updaterclient/getcurrent');
}
export const getUpdaterServer = () => {
return sendWebsocketMsg('updaterclient/getserver');
}
export const confirmServer = (version) => {
return sendWebsocketMsg('updaterclient/confirmserver', version);
}
export const exitServer = () => {
return sendWebsocketMsg('updaterclient/exitserver');
}

View File

@@ -46,6 +46,9 @@
</el-col>
</el-row>
</el-form-item>
<el-form-item label="更新密钥" prop="updaterSecretKey">
<el-input v-model="state.form.updaterSecretKey" maxlength="36" show-word-limit />
</el-form-item>
</el-form>
</div>
</template>
@@ -66,6 +69,8 @@ export default {
webPort:globalData.value.config.Server.SForward.WebPort,
tunnelPort1:globalData.value.config.Server.SForward.TunnelPortRange[0],
tunnelPort2:globalData.value.config.Server.SForward.TunnelPortRange[1],
updaterSecretKey:globalData.value.config.Server.Updater.SecretKey,
},
rules: {
relaySecretKey: [{ required: true, message: "必填", trigger: "blur" }],
@@ -142,6 +147,9 @@ export default {
SecretKey: state.form.sForwardSecretKey,
WebPort: +state.form.webPort,
TunnelPortRange: [+state.form.tunnelPort1, +state.form.tunnelPort2]
},
Updater:{
SecretKey: state.form.updaterSecretKey
}
}
});

View File

@@ -1,11 +1,28 @@
<template>
<div class="status-server-wrap" :class="{ connected: state.connected }">
<a href="javascript:;" @click="handleConfig">
<el-icon size="16"><Promotion /></el-icon>
<template v-if="state.connected">信标服务器</template>
<template v-else>信标服务器</template>
<a href="javascript:;" @click="handleConfig"> <el-icon size="16"><Promotion /></el-icon> 信标服务器</a>
<a href="javascript:;" @click="handleUpdate" class="download" :title="updateText()" :class="updateColor()">
<span>{{state.version}}</span>
<template v-if="updaterCurrent.Version">
<template v-if="updaterCurrent.Status == 1">
<el-icon size="14" class="loading"><Loading /></el-icon>
</template>
<template v-else-if="updaterServer.Status == 2">
<el-icon size="14"><Download /></el-icon>
</template>
<template v-else-if="updaterServer.Status == 3 || updaterServer.Status == 5">
<el-icon size="14" class="loading"><Loading /></el-icon>
<span class="progress" v-if="updaterServer.Length ==0">0%</span>
<span class="progress" v-else>{{parseInt(updaterServer.Current/updaterServer.Length*100)}}%</span>
</template>
<template v-else-if="updaterServer.Status == 6">
<el-icon size="14" class="yellow"><CircleCheck /></el-icon>
</template>
</template>
<template v-else>
<el-icon size="14"><Download /></el-icon>
</template>
</a>
<a href="javascript:;" class="">{{state.version}}</a>
</div>
<el-dialog v-model="state.show" title="连接设置" width="300">
<div>
@@ -29,22 +46,26 @@
<script>
import { setSignIn } from '@/apis/signin';
import { injectGlobalData } from '@/provide';
import { ElMessage } from 'element-plus';
import { computed, reactive } from 'vue';
import {Promotion} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus';
import { computed, onMounted, reactive, ref } from 'vue';
import {Promotion,Download,Loading,CircleCheck} from '@element-plus/icons-vue'
import { confirmServer, exitServer, getUpdaterCurrent, getUpdaterServer } from '@/apis/updater';
import { subWebsocketState } from '@/apis/request';
export default {
components:{Promotion},
components:{Promotion,Download,Loading,CircleCheck},
setup(props) {
const globalData = injectGlobalData();
const updaterCurrent = ref({Version: '', Status: 0, Length: 0, Current: 0});
const updaterServer = ref({Version: '', Status: 0, Length: 0, Current: 0});
const state = reactive({
show: false,
loading: false,
connected: computed(() => globalData.value.signin.Connected),
connecting: computed(() => globalData.value.signin.Connecting),
version: computed(() => globalData.value.signin.Version),
server: computed(() => globalData.value.config.Client.Server),
serverLength: computed(() => (globalData.value.config.Running.Client.Servers || []).length),
form: {
name: globalData.value.config.Client.Name,
groupid: globalData.value.config.Client.GroupId,
@@ -52,6 +73,95 @@ export default {
rules: {},
});
const _getUpdaterCurrent = ()=>{
getUpdaterCurrent().then((res)=>{
updaterCurrent.value.Version = res.Version;
updaterCurrent.value.Status = res.Status;
updaterCurrent.value.Length = res.Length;
updaterCurrent.value.Current = res.Current;
setTimeout(()=>{
_getUpdaterCurrent();
},1000);
}).catch(()=>{
setTimeout(()=>{
_getUpdaterCurrent();
},1000);
})
}
const _getUpdaterServer = ()=>{
getUpdaterServer().then((res)=>{
updaterServer.value.Version = res.Version;
updaterServer.value.Status = res.Status;
updaterServer.value.Length = res.Length;
updaterServer.value.Current = res.Current;
if(updaterServer.value.Status > 2 && updaterServer.value.Status < 6){
setTimeout(()=>{
_getUpdaterServer();
},1000);
}
}).catch(()=>{
setTimeout(()=>{
_getUpdaterServer();
},1000);
});
}
const updateText = ()=>{
if(!updaterCurrent.value.Version){
return '未检测到更新';
}
if(updaterServer.value.Status <= 2) {
return state.version != updaterCurrent.value.Version
? `不是最新版本(${updaterCurrent.value.Version}),建议更新`
: '是最新版本,但我无法阻止你喜欢更新'
}
return {
3:'正在下载',
4:'已下载',
5:'正在解压',
6:'已解压,请重启',
}[updaterServer.value.Status];
}
const updateColor = ()=>{
return state.version != updaterCurrent.value.Version ? 'yellow' :'green'
}
const handleUpdate = ()=>{
if(!updaterCurrent.value.Version){
ElMessage.error('未检测到更新');
return;
}
//未检测,检测中,下载中,解压中
if([0,1,3,5].indexOf(updaterServer.value.Status)>=0){
ElMessage.error('操作中,请稍后!');
return;
}
//已解压
if(updaterServer.value.Status == 6){
ElMessageBox.confirm('确定关闭服务端吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
exitServer();
}).catch(() => {});
return;
}
//已检测
if(updaterCurrent.value.Status == 2){
ElMessageBox.confirm('确定更新服务端吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
confirmServer(updaterCurrent.value.Version).then(()=>{
setTimeout(()=>{
_getUpdaterServer();
},1000);
});
}).catch(() => {});
}
}
const handleConfig = () => {
state.form.name = globalData.value.config.Client.Name;
state.form.groupid = globalData.value.config.Client.GroupId;
@@ -69,19 +179,33 @@ export default {
});
}
onMounted(() => {
subWebsocketState((state)=>{
if(state){
_getUpdaterCurrent();
_getUpdaterServer();
};
});
});
return {
state, handleConfig, handleSave,
state, handleConfig, handleSave,updaterCurrent,updaterServer,handleUpdate,updateText,updateColor
}
}
}
</script>
<style lang="stylus" scoped>
@keyframes loading {
from{transform:rotate(0deg)}
to{transform:rotate(360deg)}
}
.status-server-wrap{
padding-right:.5rem;
a{color:#333;}
a+a{margin-left:.6rem;}
span{border-radius:1rem;background-color:rgba(0,0,0,0.1);padding:0 .6rem; margin-left:.2rem}
&.connected {
a{color:green;font-weight:bold;}
@@ -90,6 +214,20 @@ export default {
.el-icon{
vertical-align:text-bottom;
}
a.download{
&.green{color:green}
&.red{color:red}
&.yellow{color:#e68906}
.el-icon{
font-weight:bold;
&.yellow{color:#e68906}
&.loading{
animation:loading 1s linear infinite;
}
margin-left:.3rem
}
}
}
</style>

View File

@@ -18,8 +18,8 @@
<p class="flex">
<span>{{ scope.row.IP }}</span>
<span class="flex-1"></span>
<a href="javascript:;" class="download" title="下载更新" @click="handleUpdate(scope.row)">
<span :title="updateText(scope.row)" :class="updateColor(scope.row)">
<a href="javascript:;" class="download" title="下载更新" @click="handleUpdate(scope.row)" :title="updateText(scope.row)" :class="updateColor(scope.row)">
<span>
<span>{{scope.row.Version}}</span>
<template v-if="updater.list[scope.row.MachineId]">
<template v-if="updater.list[scope.row.MachineId].Status == 1">
@@ -34,7 +34,7 @@
<span class="progress" v-else>{{parseInt(updater.list[scope.row.MachineId].Current/updater.list[scope.row.MachineId].Length*100)}}%</span>
</template>
<template v-else-if="updater.list[scope.row.MachineId].Status == 6">
<el-icon size="14" class="green"><CircleCheck /></el-icon>
<el-icon size="14" class="yellow"><CircleCheck /></el-icon>
</template>
</template>
<template v-else>
@@ -51,7 +51,7 @@
import { injectGlobalData } from '@/provide';
import { computed, ref,h } from 'vue';
import {StarFilled,Search,Download,Loading,CircleCheck} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox,ElSelect,ElOption, arrowMiddleware } from 'element-plus';
import { ElMessage, ElMessageBox,ElSelect,ElOption } from 'element-plus';
import { confirm, exit } from '@/apis/updater';
import { useUpdater } from './updater';
@@ -64,7 +64,7 @@ export default {
const globalData = injectGlobalData();
const updater = useUpdater();
const serverVersion = computed(()=>globalData.value.signin.Version);
const updaterVersion = computed(()=>updater.value.version);
const updaterVersion = computed(()=>updater.value.current.Version);
const updateText = (row)=>{
if(!updater.value.list[row.MachineId]){
@@ -74,7 +74,7 @@ export default {
return row.Version != serverVersion.value
? `与服务器版本(${serverVersion.value})不一致,建议更新`
: updaterVersion.value != row.Version
? `不是最新版本(${updaterVersion.value}),建议更新` : '版本一致,但我无法阻止你喜欢更新'
? `不是最新版本(${updaterVersion.value}),建议更新` : '是最新版本,但我无法阻止你喜欢更新'
}
return {
3:'正在下载',
@@ -170,22 +170,17 @@ a{
a.download{
margin-left:.6rem
&.green{color:green}
&.red{color:red}
&.yellow{color:#e68906}
.el-icon{
vertical-align:middle;font-weight:bold;
&.green{color:green}
&.red{color:red}
&.yellow{color:#e68906}
&.loading{
animation:loading 1s linear infinite;
}
margin-left:.6rem
}
span{
&.green{color:green}
&.red{color:red}
&.yellow{color:#e68906}
margin-left:.3rem
}
}

View File

@@ -8,7 +8,7 @@ export const provideUpdater = () => {
const updater = ref({
timer: 0,
list: {},
version: ''
current: { Version: '', Status: 0, Length: 0, Current: 0 }
});
provide(updaterSymbol, updater);
const _getUpdater = () => {
@@ -16,7 +16,10 @@ export const provideUpdater = () => {
getUpdater().then((res) => {
const self = Object.values(res).filter(c => !!c.Version)[0];
if (self) {
updater.value.version = self.Version;
updater.value.current.Version = self.Version;
updater.value.current.Status = self.Status;
updater.value.current.Length = self.Length;
updater.value.current.Current = self.Current;
}
updater.value.list = res;
updater.value.timer = setTimeout(_getUpdater, 800);

View File

@@ -16,8 +16,7 @@ export default {
components:{},
setup(props) {
const files = require.context('./', true, /.+\.vue/);
const settingComponents = files.keys().filter(c=>c != './Index.vue').map(c => files(c).default).sort((a,b)=>a.order-b.order);
const settingComponents = files.keys().filter(c=>c != './Index.vue' && c != './Version.vue').map(c => files(c).default).sort((a,b)=>a.order-b.order);
const globalData = injectGlobalData();
const state = reactive({
tab:settingComponents[0].name,

View File

@@ -16,7 +16,7 @@ import { ElMessage } from 'element-plus';
import { computed, inject, onMounted, reactive } from 'vue'
import Version from './Version.vue';
export default {
label:'服务器代理穿透',
label:'服务器穿透',
name:'sforward',
order:5,
components:{Version},

View File

@@ -81,7 +81,7 @@ import { ElMessage } from 'element-plus';
import { computed, inject, onMounted, reactive } from 'vue'
import Version from './Version.vue';
export default {
label:'外网端口服务器',
label:'端口服务器',
name:'tunnelServers',
order:1,
components:{Version},

View File

@@ -0,0 +1,57 @@
<template>
<Version ckey="updater"/>
<div style="width: 30rem;padding: 5rem 0; margin: 0 auto;">
<p class="t-c">
服务器更新密钥
</p>
<p>
<el-input type="password" show-password v-model="state.secretKey" maxlength="36" @blur="handleChange" />
</p>
</div>
</template>
<script>
import { getSecretKey,setSecretKey } from '@/apis/updater';
import { injectGlobalData } from '@/provide';
import { ElMessage } from 'element-plus';
import { computed, inject, onMounted, reactive } from 'vue'
import Version from './Version.vue';
export default {
label:'服务器更新',
name:'updater',
order:6,
components:{Version},
setup(props) {
const globalData = injectGlobalData();
const state = reactive({
secretKey:''
});
const _getSecretKey = ()=>{
getSecretKey().then((res)=>{
state.secretKey = res;
});
}
const _setSecretKey = ()=>{
if(!state.secretKey) return;
setSecretKey(state.secretKey).then(()=>{
ElMessage.success('已操作');
}).catch(()=>{
ElMessage.success('操作失败');
});
}
const handleChange = ()=>{
_setSecretKey();
}
onMounted(()=>{
_getSecretKey();
});
return {state,handleChange}
}
}
</script>
<style lang="stylus" scoped>
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div class="running-version-wrap">
<span>配置版本 : {{version}}</span>
<span>配置版本 : {{version || 1}}</span>
<el-button size="small" @click=handleEdit>手动修改版本</el-button>
<span>高版本一端自动同步到低版本一端</span>
</div>

View File

@@ -6,14 +6,16 @@ RUN echo "https://mirrors.tuna.tsinghua.edu.cn/alpine/latest-stable/main/" > /et
&& ln -snf /usr/share/zoneinfo/$clTZ /etc/localtime \
&& echo $TZ > /etc/timezone
EXPOSE 1800/tcp
EXPOSE 1801/tcp
EXPOSE 1802/tcp
EXPOSE 1802/udp
EXPOSE 1803/tcp
EXPOSE 1803/udp
EXPOSE 1804/tcp
EXPOSE 1804/udp
WORKDIR /app
COPY . .
ENTRYPOINT ["./linker.run"]
ENTRYPOINT ["./linker"]

View File

@@ -162,7 +162,7 @@ namespace linker.config
#if DEBUG
private LoggerTypes loggerType { get; set; } = LoggerTypes.DEBUG;
public bool Install { get; set; } = true;
public bool Install { get; set; } = false;
#else
private LoggerTypes loggerType { get; set; } = LoggerTypes.WARNING;
public bool Install { get; set; } = false;

BIN
linker/msquic-arm64.dll Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -39,9 +39,12 @@ namespace linker.plugins.config
{
config.Data.Server.ServicePort = info.Server.ServicePort;
config.Data.Server.Relay.SecretKey = info.Server.Relay.SecretKey;
config.Data.Server.SForward.SecretKey = info.Server.SForward.SecretKey;
config.Data.Server.SForward.WebPort = info.Server.SForward.WebPort;
config.Data.Server.SForward.TunnelPortRange = info.Server.SForward.TunnelPortRange;
config.Data.Server.Updater.SecretKey = info.Server.Updater.SecretKey;
}
config.Data.Common.Modes = info.Common.Modes;
@@ -70,6 +73,11 @@ namespace linker.plugins.config
public int ServicePort { get; set; }
public ConfigInstallServerRelayInfo Relay { get; set; }
public ConfigInstallServerSForwardInfo SForward { get; set; }
public ConfigInstallServerUpdaterInfo Updater { get; set; }
}
public sealed class ConfigInstallServerUpdaterInfo
{
public string SecretKey { get; set; }
}
public sealed class ConfigInstallServerRelayInfo
{

View File

@@ -8,24 +8,80 @@ using MemoryPack;
using System.Collections.Concurrent;
using linker.plugins.updater.config;
using linker.libs.extends;
using linker.client.config;
namespace linker.plugins.updater
{
public sealed class UpdaterClientApiController : IApiClientController
{
private readonly MessengerSender messengerSender;
private readonly UpdaterTransfer updaterTransfer;
private readonly UpdaterClientTransfer updaterTransfer;
private readonly ClientSignInState clientSignInState;
private readonly FileConfig config;
private readonly UpdaterClientTransfer updaterClientTransfer;
private readonly RunningConfig runningConfig;
public UpdaterClientApiController(MessengerSender messengerSender, UpdaterTransfer updaterTransfer, ClientSignInState clientSignInState, FileConfig config)
public UpdaterClientApiController(MessengerSender messengerSender, UpdaterClientTransfer updaterTransfer, ClientSignInState clientSignInState, FileConfig config, UpdaterClientTransfer updaterClientTransfer, RunningConfig runningConfig)
{
this.messengerSender = messengerSender;
this.updaterTransfer = updaterTransfer;
this.clientSignInState = clientSignInState;
this.config = config;
this.updaterClientTransfer = updaterClientTransfer;
this.runningConfig = runningConfig;
}
public string GetSecretKey(ApiControllerParamsInfo param)
{
return updaterClientTransfer.GetSecretKey();
}
public void SetSecretKey(ApiControllerParamsInfo param)
{
updaterClientTransfer.SetSecretKey(param.Content);
}
public UpdateInfo GetCurrent(ApiControllerParamsInfo param)
{
var updaters = updaterTransfer.Get();
if(updaters.TryGetValue(config.Data.Client.Id,out UpdateInfo info))
{
return info;
}
return new UpdateInfo { };
}
public async Task<UpdateInfo> GetServer(ApiControllerParamsInfo param)
{
MessageResponeInfo resp = await messengerSender.SendReply(new MessageRequestWrap
{
Connection = clientSignInState.Connection,
MessengerId = (ushort)UpdaterMessengerIds.UpdateServer,
});
if (resp.Code == MessageResponeCodes.OK && resp.Data.Length > 0)
{
return MemoryPackSerializer.Deserialize<UpdateInfo>(resp.Data.Span);
}
return new UpdateInfo();
}
public async Task ConfirmServer(ApiControllerParamsInfo param)
{
await messengerSender.SendOnly(new MessageRequestWrap
{
Connection = clientSignInState.Connection,
MessengerId = (ushort)UpdaterMessengerIds.ConfirmServer,
Payload = MemoryPackSerializer.Serialize(new UpdaterConfirmServerInfo { SecretKey = runningConfig.Data.UpdaterSecretKey, Version = param.Content })
});
}
public async Task ExitServer(ApiControllerParamsInfo param)
{
await messengerSender.SendOnly(new MessageRequestWrap
{
Connection = clientSignInState.Connection,
MessengerId = (ushort)UpdaterMessengerIds.ExitServer,
Payload = MemoryPackSerializer.Serialize(new UpdaterConfirmServerInfo { SecretKey = runningConfig.Data.UpdaterSecretKey, Version = string.Empty })
});
}
public ConcurrentDictionary<string, UpdateInfo> Get(ApiControllerParamsInfo param)
{
return updaterTransfer.Get();
@@ -52,7 +108,7 @@ namespace linker.plugins.updater
{
if (string.IsNullOrWhiteSpace(param.Content) || param.Content == config.Data.Client.Id)
{
updaterTransfer.Exit();
Environment.Exit(1);
}
else
{

View File

@@ -0,0 +1,129 @@
using linker.client;
using linker.client.config;
using linker.config;
using linker.plugins.updater.messenger;
using linker.server;
using MemoryPack;
using System.Collections.Concurrent;
namespace linker.plugins.updater
{
public sealed class UpdaterClientTransfer
{
private UpdateInfo updateInfo = new UpdateInfo();
private ConcurrentDictionary<string, UpdateInfo> updateInfos = new ConcurrentDictionary<string, UpdateInfo>();
private readonly FileConfig fileConfig;
private readonly MessengerSender messengerSender;
private readonly ClientSignInState clientSignInState;
private readonly UpdaterHelper updaterHelper;
private readonly RunningConfig running;
private readonly RunningConfigTransfer runningConfigTransfer;
private string configKey = "updater";
public UpdaterClientTransfer(FileConfig fileConfig, MessengerSender messengerSender, ClientSignInState clientSignInState, UpdaterHelper updaterHelper, RunningConfig running, RunningConfigTransfer runningConfigTransfer)
{
this.fileConfig = fileConfig;
this.messengerSender = messengerSender;
this.clientSignInState = clientSignInState;
this.updaterHelper = updaterHelper;
this.running = running;
this.runningConfigTransfer = runningConfigTransfer;
runningConfigTransfer.Setter(configKey, SetSecretKey);
runningConfigTransfer.Getter(configKey, () => MemoryPackSerializer.Serialize(GetSecretKey()));
clientSignInState.NetworkFirstEnabledHandle += () =>
{
LoadTask();
UpdateTask();
};
}
public string GetSecretKey()
{
return running.Data.UpdaterSecretKey;
}
public void SetSecretKey(string key)
{
running.Data.UpdaterSecretKey = key;
running.Data.Update();
runningConfigTransfer.IncrementVersion(configKey);
SyncKey();
}
private void SetSecretKey(Memory<byte> data)
{
running.Data.UpdaterSecretKey = MemoryPackSerializer.Deserialize<string>(data.Span);
running.Data.Update();
}
private void SyncKey()
{
runningConfigTransfer.Sync(configKey, MemoryPackSerializer.Serialize(GetSecretKey()));
}
/// <summary>
/// 所有客户端的更新信息
/// </summary>
/// <returns></returns>
public ConcurrentDictionary<string, UpdateInfo> Get()
{
return updateInfos;
}
/// <summary>
/// 确认更新
/// </summary>
public void Confirm(string version)
{
updaterHelper.Confirm(updateInfo, version);
}
/// <summary>
/// 来自别的客户端的更新信息
/// </summary>
/// <param name="info"></param>
public void Update(UpdateInfo info)
{
if (string.IsNullOrWhiteSpace(info.MachineId) == false)
{
updateInfos.AddOrUpdate(info.MachineId, info, (a, b) => info);
}
}
private void UpdateTask()
{
Task.Run(async () =>
{
while (true)
{
if (updateInfo.StatusChanged())
{
updateInfo.MachineId = fileConfig.Data.Client.Id;
await messengerSender.SendOnly(new MessageRequestWrap
{
Connection = clientSignInState.Connection,
MessengerId = (ushort)UpdaterMessengerIds.UpdateForward,
Payload = MemoryPackSerializer.Serialize(updateInfo),
});
Update(updateInfo);
}
await Task.Delay(1000);
}
});
}
private void LoadTask()
{
Task.Run(async () =>
{
while (true)
{
await updaterHelper.GetUpdateInfo(updateInfo);
await Task.Delay(60000);
}
});
}
}
}

View File

@@ -1,10 +1,5 @@
using linker.client;
using linker.config;
using linker.libs;
using linker.plugins.updater.messenger;
using linker.server;
using linker.libs;
using MemoryPack;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO.Compression;
using System.Runtime.InteropServices;
@@ -12,112 +7,20 @@ using System.Text.RegularExpressions;
namespace linker.plugins.updater
{
public sealed class UpdaterTransfer
public sealed class UpdaterHelper
{
private UpdateInfo updateInfo = new UpdateInfo();
private ConcurrentDictionary<string, UpdateInfo> updateInfos = new ConcurrentDictionary<string, UpdateInfo>();
private readonly FileConfig fileConfig;
private readonly MessengerSender messengerSender;
private readonly ClientSignInState clientSignInState;
public UpdaterTransfer(FileConfig fileConfig, MessengerSender messengerSender, ClientSignInState clientSignInState)
private string[] extractExcludeFiles = new string[] { "msquic.dll", "msquic-openssl.dll", "tun2socks", "tun2socks.exe" };
public UpdaterHelper()
{
this.fileConfig = fileConfig;
this.messengerSender = messengerSender;
this.clientSignInState = clientSignInState;
clientSignInState.NetworkFirstEnabledHandle += () =>
{
LoadTask();
UpdateTask();
};
StartClearTempFile();
ClearFiles();
}
/// <summary>
/// 所有客户端的更新信息
/// 获取更新信息
/// </summary>
/// <param name="updateInfo"></param>
/// <returns></returns>
public ConcurrentDictionary<string, UpdateInfo> Get()
{
return updateInfos;
}
/// <summary>
/// 确认更新
/// </summary>
public void Confirm(string version)
{
Task.Run(async () =>
{
string fileName = Path.GetFileName(Process.GetCurrentProcess().MainModule.FileName);
await DownloadUpdate(version);
await ExtractUpdate();
if (OperatingSystem.IsLinux())
{
string result = CommandHelper.Linux(string.Empty, new string[] { $"chmod a+x {fileName}" });
}
else if (OperatingSystem.IsMacOS())
{
string result = CommandHelper.Osx(string.Empty, new string[] { $"chmod a+x {fileName}" });
}
});
}
/// <summary>
/// 关闭程序
/// </summary>
public void Exit()
{
Environment.Exit(1);
}
/// <summary>
/// 来自别的客户端的更新信息
/// </summary>
/// <param name="info"></param>
public void Update(UpdateInfo info)
{
if (string.IsNullOrWhiteSpace(info.MachineId) == false)
{
updateInfos.AddOrUpdate(info.MachineId, info, (a, b) => info);
}
}
private void UpdateTask()
{
Task.Run(async () =>
{
while (true)
{
if (updateInfo.StatusChanged())
{
updateInfo.MachineId = fileConfig.Data.Client.Id;
await messengerSender.SendOnly(new MessageRequestWrap
{
Connection = clientSignInState.Connection,
MessengerId = (ushort)UpdaterMessengerIds.UpdateForward,
Payload = MemoryPackSerializer.Serialize(updateInfo),
});
Update(updateInfo);
}
await Task.Delay(1000);
}
});
}
private void LoadTask()
{
Task.Run(async () =>
{
while (true)
{
await GetUpdateInfo();
await Task.Delay(60000);
}
});
}
private async Task GetUpdateInfo()
public async Task GetUpdateInfo(UpdateInfo updateInfo)
{
//正在检查,或者已经确认更新了
if (updateInfo.Status == UpdateStatus.Checking || updateInfo.Status > UpdateStatus.Checked)
@@ -147,85 +50,13 @@ namespace linker.plugins.updater
updateInfo.Status = status;
}
}
private async Task ExtractUpdate()
{
//没下载完成
if (updateInfo.Status != UpdateStatus.Downloaded)
{
return;
}
UpdateStatus status = updateInfo.Status;
try
{
updateInfo.Status = UpdateStatus.Extracting;
updateInfo.Current = 0;
updateInfo.Length = 0;
using ZipArchive archive = ZipFile.OpenRead("updater.zip");
updateInfo.Length = archive.Entries.Sum(c => c.Length);
string configPath = Path.GetFullPath("./configs");
foreach (ZipArchiveEntry entry in archive.Entries)
{
string entryPath = Path.GetFullPath(Path.Join("./", entry.FullName.Substring(entry.FullName.IndexOf('/'))));
if (entryPath.EndsWith('\\') || entryPath.EndsWith('/'))
{
continue;
}
if (entryPath.StartsWith(configPath))
{
continue;
}
if (Directory.Exists(Path.GetDirectoryName(entryPath)) == false)
{
Directory.CreateDirectory(Path.GetDirectoryName(entryPath));
}
if (File.Exists(entryPath))
{
try
{
File.Move(entryPath, $"{entryPath}.temp", true);
}
catch (Exception)
{
continue;
}
}
using Stream entryStream = entry.Open();
using FileStream fileStream = File.Create(entryPath);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = await entryStream.ReadAsync(buffer)) != 0)
{
await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead));
updateInfo.Current += bytesRead;
}
entryStream.Dispose();
fileStream.Flush();
fileStream.Dispose();
}
archive.Dispose();
File.Delete("updater.zip");
updateInfo.Status = UpdateStatus.Extracted;
}
catch (Exception ex)
{
if (LoggerHelper.Instance.LoggerLevel <= LoggerTypes.DEBUG)
{
LoggerHelper.Instance.Error(ex);
}
updateInfo.Status = status;
}
}
private async Task DownloadUpdate(string version)
/// <summary>
/// 下载更新
/// </summary>
/// <param name="updateInfo"></param>
/// <param name="version"></param>
/// <returns></returns>
public async Task DownloadUpdate(UpdateInfo updateInfo, string version)
{
if (updateInfo.Status != UpdateStatus.Checked)
{
@@ -277,12 +108,114 @@ namespace linker.plugins.updater
updateInfo.Status = status;
}
}
private void StartClearTempFile()
/// <summary>
/// 解压更新
/// </summary>
/// <param name="updateInfo"></param>
/// <returns></returns>
public async Task ExtractUpdate(UpdateInfo updateInfo)
{
ClearTempFile();
//没下载完成
if (updateInfo.Status != UpdateStatus.Downloaded)
{
return;
}
string fileName = Path.GetFileName(Process.GetCurrentProcess().MainModule.FileName);
UpdateStatus status = updateInfo.Status;
try
{
updateInfo.Status = UpdateStatus.Extracting;
updateInfo.Current = 0;
updateInfo.Length = 0;
using ZipArchive archive = ZipFile.OpenRead("updater.zip");
updateInfo.Length = archive.Entries.Sum(c => c.Length);
foreach (ZipArchiveEntry entry in archive.Entries)
{
string entryPath = Path.GetFullPath(Path.Join("./", entry.FullName.Substring(entry.FullName.IndexOf('/'))));
if (entryPath.EndsWith('\\') || entryPath.EndsWith('/'))
{
continue;
}
if (extractExcludeFiles.Contains(Path.GetFileName(entryPath)))
{
continue;
}
if (Directory.Exists(Path.GetDirectoryName(entryPath)) == false)
{
Directory.CreateDirectory(Path.GetDirectoryName(entryPath));
}
if (File.Exists(entryPath))
{
try
{
File.Move(entryPath, $"{entryPath}.temp", true);
}
catch (Exception)
{
continue;
}
}
using Stream entryStream = entry.Open();
using FileStream fileStream = File.Create(entryPath);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = await entryStream.ReadAsync(buffer)) != 0)
{
await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead));
updateInfo.Current += bytesRead;
}
entryStream.Dispose();
fileStream.Flush();
fileStream.Dispose();
}
archive.Dispose();
File.Delete("updater.zip");
updateInfo.Status = UpdateStatus.Extracted;
}
catch (Exception ex)
{
if (LoggerHelper.Instance.LoggerLevel <= LoggerTypes.DEBUG)
{
LoggerHelper.Instance.Error(ex);
}
updateInfo.Status = status;
}
if (OperatingSystem.IsLinux())
{
string result = CommandHelper.Linux(string.Empty, new string[] { $"chmod a+x {fileName}" });
}
else if (OperatingSystem.IsMacOS())
{
string result = CommandHelper.Osx(string.Empty, new string[] { $"chmod a+x {fileName}" });
}
}
private void ClearTempFile(string path = "./")
public void Confirm(UpdateInfo updateInfo, string version)
{
Task.Run(async () =>
{
await DownloadUpdate(updateInfo, version);
await ExtractUpdate(updateInfo);
});
}
/// <summary>
/// 清理旧文件
/// </summary>
private void ClearFiles()
{
ClearTempFiles();
}
private void ClearTempFiles(string path = "./")
{
string fullPath = Path.GetFullPath(path);
@@ -298,10 +231,9 @@ namespace linker.plugins.updater
}
foreach (var item in Directory.GetDirectories(fullPath))
{
ClearTempFile(item);
ClearTempFiles(item);
}
}
}
[MemoryPackable]

View File

@@ -0,0 +1,27 @@
namespace linker.plugins.updater
{
public sealed class UpdaterServerTransfer
{
private UpdateInfo updateInfo = new UpdateInfo { Status = UpdateStatus.Checked };
private readonly UpdaterHelper updaterHelper;
public UpdaterServerTransfer(UpdaterHelper updaterHelper)
{
this.updaterHelper = updaterHelper;
}
public UpdateInfo Get()
{
return updateInfo;
}
/// <summary>
/// 确认更新
/// </summary>
public void Confirm(string version)
{
updaterHelper.Confirm(updateInfo, version);
}
}
}

View File

@@ -23,24 +23,29 @@ namespace linker.plugins.updater
public void AddClient(ServiceCollection serviceCollection, FileConfig config, Assembly[] assemblies)
{
serviceCollection.AddSingleton<UpdaterTransfer>();
serviceCollection.AddSingleton<UpdaterHelper>();
serviceCollection.AddSingleton<UpdaterClientTransfer>();
serviceCollection.AddSingleton<UpdaterClientApiController>();
serviceCollection.AddSingleton<UpdaterClientMessenger>();
serviceCollection.AddSingleton<UpdaterClientApiController>();
}
public void AddServer(ServiceCollection serviceCollection, FileConfig config, Assembly[] assemblies)
{
serviceCollection.AddSingleton<UpdaterHelper>();
serviceCollection.AddSingleton<UpdaterServerTransfer>();
serviceCollection.AddSingleton<UpdaterServerMessenger>();
}
public void UseClient(ServiceProvider serviceProvider, FileConfig config, Assembly[] assemblies)
{
UpdaterTransfer updaterTransfer = serviceProvider.GetService<UpdaterTransfer>();
_ = serviceProvider.GetService<UpdaterClientTransfer>();
}
public void UseServer(ServiceProvider serviceProvider, FileConfig config, Assembly[] assemblies)
{
_ = serviceProvider.GetService<UpdaterServerTransfer>();
}
}
}

View File

@@ -1,5 +1,19 @@
using MemoryPack;
namespace linker.client.config
{
public sealed partial class RunningConfigInfo
{
/// <summary>
/// 自动更新密钥
/// </summary>
public string UpdaterSecretKey { get; set; } = "snltty";
}
}
namespace linker.plugins.updater.config
{
[MemoryPackable]
@@ -8,4 +22,33 @@ namespace linker.plugins.updater.config
public string MachineId { get; set; }
public string Version { get; set; }
}
[MemoryPackable]
public sealed partial class UpdaterConfirmServerInfo
{
public string SecretKey { get; set; }
public string Version { get; set; }
}
}
namespace linker.config
{
public partial class ConfigServerInfo
{
/// <summary>
/// 服务器穿透配置
/// </summary>
public UpdaterConfigServerInfo Updater { get; set; } = new UpdaterConfigServerInfo();
}
public sealed class UpdaterConfigServerInfo
{
/// <summary>
/// 密钥
/// </summary>
public string SecretKey { get; set; } = Guid.NewGuid().ToString().ToUpper();
}
}

View File

@@ -1,4 +1,5 @@
using linker.plugins.signin.messenger;
using linker.config;
using linker.plugins.signin.messenger;
using linker.plugins.updater.config;
using linker.server;
using MemoryPack;
@@ -7,8 +8,8 @@ namespace linker.plugins.updater.messenger
{
public sealed class UpdaterClientMessenger : IMessenger
{
private readonly UpdaterTransfer updaterTransfer;
public UpdaterClientMessenger(UpdaterTransfer updaterTransfer)
private readonly UpdaterClientTransfer updaterTransfer;
public UpdaterClientMessenger(UpdaterClientTransfer updaterTransfer)
{
this.updaterTransfer = updaterTransfer;
}
@@ -41,7 +42,7 @@ namespace linker.plugins.updater.messenger
[MessengerId((ushort)UpdaterMessengerIds.Exit)]
public void Exit(IConnection connection)
{
updaterTransfer.Exit();
Environment.Exit(1);
}
}
@@ -50,12 +51,52 @@ namespace linker.plugins.updater.messenger
{
private readonly MessengerSender messengerSender;
private readonly SignCaching signCaching;
private readonly UpdaterServerTransfer updaterServerTransfer;
private readonly FileConfig fileConfig;
public UpdaterServerMessenger(MessengerSender messengerSender, SignCaching signCaching)
public UpdaterServerMessenger(MessengerSender messengerSender, SignCaching signCaching, UpdaterServerTransfer updaterServerTransfer, FileConfig fileConfig)
{
this.messengerSender = messengerSender;
this.signCaching = signCaching;
this.updaterServerTransfer = updaterServerTransfer;
this.fileConfig = fileConfig;
}
/// <summary>
/// 获取服务器的更新信息
/// </summary>
/// <param name="connection"></param>
[MessengerId((ushort)UpdaterMessengerIds.UpdateServer)]
public void UpdateServer(IConnection connection)
{
connection.Write(MemoryPackSerializer.Serialize(updaterServerTransfer.Get()));
}
/// <summary>
/// 开始更新服务器
/// </summary>
/// <param name="connection"></param>
[MessengerId((ushort)UpdaterMessengerIds.ConfirmServer)]
public void ConfirmServer(IConnection connection)
{
UpdaterConfirmServerInfo confirm = MemoryPackSerializer.Deserialize<UpdaterConfirmServerInfo>(connection.ReceiveRequestWrap.Payload.Span);
if(fileConfig.Data.Server.Updater.SecretKey == confirm.SecretKey)
{
updaterServerTransfer.Confirm(confirm.Version);
}
}
/// <summary>
/// 关闭服务器
/// </summary>
/// <param name="connection"></param>
[MessengerId((ushort)UpdaterMessengerIds.ExitServer)]
public void ExitServer(IConnection connection)
{
UpdaterConfirmServerInfo confirm = MemoryPackSerializer.Deserialize<UpdaterConfirmServerInfo>(connection.ReceiveRequestWrap.Payload.Span);
if (fileConfig.Data.Server.Updater.SecretKey == confirm.SecretKey)
{
Environment.Exit(1);
}
}
/// <summary>
/// 转发确认更新消息

View File

@@ -13,6 +13,10 @@
ExitForward = 2605,
Exit = 2606,
UpdateServer = 2607,
ConfirmServer = 2608,
ExitServer = 2609,
Max = 2299
}
}

View File

@@ -18,7 +18,6 @@ do
for r in ${rs[@]}
do
dotnet publish ./${f} -c release -f net8.0 -o ./public/publish/docker/linux-${p}-${r}/${f} -r ${p}-${r} --self-contained true -p:TieredPGO=true -p:DebugType=none -p:DebugSymbols=false -p:PublishSingleFile=true -p:PublishTrimmed=true -p:EnableCompressionInSingleFile=true -p:DebuggerSupport=false -p:EnableUnsafeBinaryFormatterSerialization=false -p:EnableUnsafeUTF7Encoding=false -p:HttpActivityPropagationSupport=false -p:InvariantGlobalization=true -p:MetadataUpdaterSupport=false -p:UseSystemResourceKeys=true -p:TrimMode=partial
cp -rf public/publish/docker/linux-${p}-${r}/${f}/${f} public/publish/docker/linux-${p}-${r}/${f}/${f}.run
rm -rf public/publish/docker/linux-${p}-${r}/${f}/${f}
cp -rf linker/Dockerfile-${p} public/publish/docker/linux-${p}-${r}/${f}/Dockerfile-${p}
cp -rf public/extends/any/* public/publish/docker/linux-${p}-${r}/${f}/*

View File

@@ -15,8 +15,10 @@ for %%r in (win-x64,win-arm64) do (
dotnet publish ./linker.service -c release -f net8.0 -o public/extends/%%r/linker-%%r/ -r %%r -p:PublishAot=true -p:PublishTrimmed=true --self-contained true -p:TieredPGO=true -p:DebugType=none -p:DebugSymbols=false -p:EnableCompressionInSingleFile=true -p:DebuggerSupport=false -p:EnableUnsafeBinaryFormatterSerialization=false -p:EnableUnsafeUTF7Encoding=false -p:HttpActivityPropagationSupport=false -p:InvariantGlobalization=true -p:MetadataUpdaterSupport=false -p:UseSystemResourceKeys=true
echo F|xcopy "linker.tray.win\\dist\\*" "public\\extends\\%%r\\linker-%%r\\*" /s /f /h /y
echo F|xcopy "linker\\msquic.dll" "public\\extends\\%%r\\linker-%%r\\msquic.dll" /s /f /h /y
echo F|xcopy "linker\\msquic-openssl.dll" "public\\extends\\%%r\\linker-%%r\\msquic-openssl.dll" /s /f /h /y
echo F|xcopy "linker\\msquic-openssl3-x64.dll" "public\\extends\\%%r\\linker-%%r\\msquic-openssl.dll" /s /f /h /y)
)
echo F|xcopy "linker\\msquic-arm64.dll" "public\\extends\\win-arm64\\linker-win-arm64\\msquic.dll" /s /f /h /y
echo F|xcopy "linker\\msquic-openssl3-arm64.dll" "public\\extends\\win-arm64\\linker-win-arm64\\msquic-openssl.dll" /s /f /h /y
for %%r in (linux-x64,linux-arm64,osx-x64,osx-arm64) do (
echo F|xcopy "linker\\plugins\\tuntap\\tun2socks-%%r" "public\\extends\\%%r\\linker-%%r\\plugins\\tuntap\\tun2socks" /s /f /h /y