소스 검색

merge master

cnlh 5 년 전
부모
커밋
39b9489c92
100개의 변경된 파일4760개의 추가작업 그리고 1654개의 파일을 삭제
  1. 58 0
      .travis.yml
  2. 2 2
      Dockerfile.npc
  3. 3 3
      Dockerfile.nps
  4. 47 995
      README.md
  5. 80 0
      README_zh.md
  6. 31 20
      bridge/bridge.go
  7. 39 0
      build.android.sh
  8. 179 0
      build.sh
  9. 85 9
      client/client.go
  10. 0 75
      client/client_test.go
  11. 14 10
      client/control.go
  12. 3 3
      client/health.go
  13. 19 7
      client/local.go
  14. 1 1
      client/register.go
  15. 117 26
      cmd/npc/npc.go
  16. 51 0
      cmd/npc/sdk.go
  17. 122 30
      cmd/nps/nps.go
  18. 7 1
      conf/nps.conf
  19. 0 0
      docs/.nojekyll
  20. 21 0
      docs/README.md
  21. 16 0
      docs/_coverpage.md
  22. 28 0
      docs/_sidebar.md
  23. 46 0
      docs/api.md
  24. 6 0
      docs/contribute.md
  25. 32 0
      docs/description.md
  26. 3 0
      docs/discuss.md
  27. 7 0
      docs/donate.md
  28. 121 0
      docs/example.md
  29. 247 0
      docs/feature.md
  30. 42 0
      docs/index.html
  31. 18 0
      docs/install.md
  32. 4 0
      docs/introduction.md
  33. BIN
      docs/logo.png
  34. 0 0
      docs/logo.svg
  35. 36 0
      docs/npc_extend.md
  36. 23 0
      docs/npc_sdk.md
  37. 107 0
      docs/nps_extend.md
  38. 45 0
      docs/nps_use.md
  39. 35 0
      docs/run.md
  40. 21 0
      docs/server_config.md
  41. 5 0
      docs/thanks.md
  42. 221 0
      docs/use.md
  43. 233 0
      docs/webapi.md
  44. 12 8
      go.mod
  45. 59 19
      go.sum
  46. 37 0
      gui/npc/AndroidManifest.xml
  47. 173 0
      gui/npc/npc.go
  48. 821 0
      image/work_flow.svg
  49. 2 2
      lib/common/const.go
  50. 48 0
      lib/common/logs.go
  51. 230 28
      lib/common/netpackager.go
  52. 7 4
      lib/common/pool.go
  53. 13 2
      lib/common/run.go
  54. 68 4
      lib/common/util.go
  55. 9 7
      lib/config/config.go
  56. 9 9
      lib/conn/conn.go
  57. 31 1
      lib/conn/link.go
  58. 1 8
      lib/conn/snappy.go
  59. 1 1
      lib/daemon/daemon.go
  60. 1 1
      lib/daemon/reload.go
  61. 3 3
      lib/file/db.go
  62. 2 2
      lib/file/file.go
  63. 2 1
      lib/file/obj.go
  64. 2 2
      lib/goroutine/pool.go
  65. 119 47
      lib/install/install.go
  66. 271 184
      lib/mux/conn.go
  67. 35 8
      lib/mux/mux.go
  68. 8 2
      lib/mux/mux_test.go
  69. 16 8
      lib/mux/pconn.go
  70. 4 2
      lib/mux/pmux.go
  71. 19 10
      lib/mux/queue.go
  72. 46 0
      lib/mux/sysGetsock_nowindows.go
  73. 46 0
      lib/mux/sysGetsock_windows.go
  74. 2 2
      lib/version/version.go
  75. 1 1
      server/connection/connection.go
  76. 4 4
      server/proxy/base.go
  77. 8 8
      server/proxy/http.go
  78. 5 5
      server/proxy/https.go
  79. 1 1
      server/proxy/p2p.go
  80. 122 20
      server/proxy/socks5.go
  81. 17 9
      server/proxy/tcp.go
  82. 2 2
      server/proxy/transport.go
  83. 1 1
      server/proxy/transport_windows.go
  84. 4 4
      server/proxy/udp.go
  85. 15 7
      server/server.go
  86. 2 2
      server/test/test.go
  87. 4 1
      server/tool/utils.go
  88. 1 1
      web/controllers/auth.go
  89. 8 5
      web/controllers/base.go
  90. 4 4
      web/controllers/client.go
  91. 6 3
      web/controllers/index.go
  92. 52 4
      web/controllers/login.go
  93. 19 7
      web/routers/router.go
  94. 2 2
      web/static/js/langchange.js
  95. 1 1
      web/static/page/error.html
  96. 189 0
      web/static/page/lang-example.xml
  97. 2 2
      web/views/client/add.html
  98. 2 2
      web/views/client/edit.html
  99. 14 9
      web/views/client/list.html
  100. 2 2
      web/views/index/add.html

+ 58 - 0
.travis.yml

@@ -0,0 +1,58 @@
+language: go
+
+go:
+  - "1.13"
+  - master
+services:
+  - docker
+script:
+  - go test -v ./cmd/nps/
+os:
+  - linux
+before_deploy:
+  - chmod +x ./build.sh && chmod +x ./build.android.sh && ./build.sh
+
+deploy:
+  provider: releases
+  api_key:
+    secure: ${TOKEN}
+  skip_cleanup: true
+  file:
+  - freebsd_386_client.tar.gz
+  - freebsd_386_server.tar.gz
+  - freebsd_amd64_client.tar.gz
+  - freebsd_amd64_server.tar.gz
+  - freebsd_arm_client.tar.gz
+  - freebsd_arm_server.tar.gz
+  - linux_386_client.tar.gz
+  - linux_386_server.tar.gz
+  - linux_amd64_client.tar.gz
+  - linux_amd64_server.tar.gz
+  - linux_arm64_client.tar.gz
+  - linux_arm64_server.tar.gz
+  - linux_arm_v5_client.tar.gz
+  - linux_arm_v6_client.tar.gz
+  - linux_arm_v7_client.tar.gz
+  - linux_arm_v5_server.tar.gz
+  - linux_arm_v6_server.tar.gz
+  - linux_arm_v7_server.tar.gz
+  - linux_mips64le_client.tar.gz
+  - linux_mips64le_server.tar.gz
+  - linux_mips64_client.tar.gz
+  - linux_mips64_server.tar.gz
+  - linux_mipsle_client.tar.gz
+  - linux_mipsle_server.tar.gz
+  - linux_mips_client.tar.gz
+  - linux_mips_server.tar.gz
+  - darwin_amd64_client.tar.gz
+  - darwin_amd64_server.tar.gz
+  - windows_386_client.tar.gz
+  - windows_386_server.tar.gz
+  - windows_amd64_client.tar.gz
+  - windows_amd64_server.tar.gz
+  - npc_syno.spk
+  - npc_sdk.tar.gz
+  - android_client.apk
+  on:
+    tags: true
+    all_branches: true

+ 2 - 2
Dockerfile.npc

@@ -1,10 +1,10 @@
 FROM golang as builder
-WORKDIR /go/src/github.com/cnlh/nps
+WORKDIR /go/src/ehang.io/nps
 COPY . .
 RUN go get -d -v ./... 
 RUN CGO_ENABLED=0 go build -ldflags="-w -s -extldflags -static" ./cmd/npc/npc.go
 
 FROM scratch
-COPY --from=builder /go/src/github.com/cnlh/nps/npc /
+COPY --from=builder /go/src/ehang.io/nps/npc /
 VOLUME /conf
 ENTRYPOINT ["/npc"]

+ 3 - 3
Dockerfile.nps

@@ -1,11 +1,11 @@
 FROM golang as builder
-WORKDIR /go/src/github.com/cnlh/nps
+WORKDIR /go/src/ehang.io/nps
 COPY . .
 RUN go get -d -v ./... 
 RUN CGO_ENABLED=0 go build -ldflags="-w -s -extldflags -static" ./cmd/nps/nps.go
 
 FROM scratch
-COPY --from=builder /go/src/github.com/cnlh/nps/nps /
-COPY --from=builder /go/src/github.com/cnlh/nps/web /web
+COPY --from=builder /go/src/ehang.io/nps/nps /
+COPY --from=builder /go/src/ehang.io/nps/web /web
 VOLUME /conf
 CMD ["/nps"]

+ 47 - 995
README.md

@@ -1,1024 +1,76 @@
 
-# nps
-![](https://img.shields.io/github/stars/cnlh/nps.svg)   ![](https://img.shields.io/github/forks/cnlh/nps.svg)
+# NPS
+![](https://img.shields.io/github/stars/ehang-io/nps.svg)   ![](https://img.shields.io/github/forks/ehang-io/nps.svg)
 [![Gitter](https://badges.gitter.im/cnlh-nps/community.svg)](https://gitter.im/cnlh-nps/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
+[![Build Status](https://travis-ci.org/ehang-io/nps.svg?branch=master)](https://travis-ci.org/ehang-io/nps)
+![GitHub All Releases](https://img.shields.io/github/downloads/ehang-io/nps/total)
 
-nps是一款轻量级、高性能、功能强大的**内网穿透**代理服务器。目前支持**tcp、udp流量转发**,可支持任何**tcp、udp**上层协议(访问内网网站、本地支付接口调试、ssh访问、远程桌面,内网dns解析等等……),此外还**支持内网http代理、内网socks5代理**、**p2p等**,并带有功能强大的web管理端。
+[README](https://github.com/ehang-io/nps/blob/master/README.md)|[中文文档](https://github.com/ehang-io/nps/blob/master/README_zh.md)
 
+NPS is a lightweight, high-performance, powerful **intranet penetration** proxy server, with a powerful web management terminal.
 
-## 背景
-![image](https://github.com/cnlh/nps/blob/master/image/web.png?raw=true)
 
-1. 做微信公众号开发、小程序开发等----> 域名代理模式
+![image](https://github.com/ehang-io/nps/blob/master/image/web.png?raw=true)
 
+## Feature
 
-2. 想在外网通过ssh连接内网的机器,做云服务器到内网服务器端口的映射,----> tcp代理模式
+- Comprehensive protocol support, compatible with almost all commonly used protocols, such as tcp, udp, http(s), socks5, p2p, http proxy ...
+- Full platform compatibility (linux, windows, macos, Qunhui, etc.), support installation as a system service simply.
+- Comprehensive control, both client and server control are allowed.
+- Https integration, support to convert backend proxy and web services to https, and support multiple certificates.
+- Just simple configuration on web ui can complete most requirements.
+- Complete information display, such as traffic, system information, real-time bandwidth, client version, etc.
+- Powerful extension functions, everything is available (cache, compression, encryption, traffic limit, bandwidth limit, port reuse, etc.)
+- Domain name resolution has functions such as custom headers, 404 page configuration, host modification, site protection, URL routing, and pan-resolution.
+- Multi-user and user registration support on server.
 
-3. 在非内网环境下使用内网dns,或者需要通过udp访问内网机器等----> udp代理模式
+**Didn't find the feature you want? It doesn't matter, click [Enter the document](https://ehang-io.github.io/nps/) to find it!**
 
-4. 在外网使用HTTP代理访问内网站点----> http代理模式
+## Quick start
 
-5. 搭建一个内网穿透ss,在外网如同使用内网vpn一样访问内网资源或者设备----> socks5代理模式
+### Installation
 
+> [releases](https://github.com/ehang-io/nps/releases)
 
-## 目录
+Download the corresponding system version, the server and client are separate.
 
-* [安装](#安装)
-    * [编译安装](#源码安装)
-    * [release安装](#release安装)
-    * [docker安装](#docker安装)
-* [使用示例(以web主控模式为主)](#使用示例)
-    * [统一准备工作](#统一准备工作(必做))
-    * [http|https域名解析](#域名解析)
-    * [内网ssh连接即tcp隧道](#tcp隧道)
-    * [内网dns解析即udp隧道](#udp隧道)
-    * [内网socks5代理](#socks5代理)
-    * [内网http正向代理](#http正向代理)
-    * [内网安全私密代理](#私密代理)
-    * [p2p穿透](#p2p服务)
-    * [简单的内网文件访问服务](#文件访问模式)
-* [服务端](#web管理)
-    * [服务端启动](#服务端启动)
-       * [服务端测试](#服务端测试)
-       * [服务端启动](#服务端启动)
-       * [web管理](#web管理)
-       * [服务端配置文件重载](#服务端配置文件重载)
-       * [服务端停止或重启](#服务端停止或重启)
-    * [配置文件说明](#服务端配置文件)
-    * [使用https](#使用https)
-    * [与nginx配合](#与nginx配合)
-    * [关闭http|https代理](#关闭代理)
-    * [将nps安装到系统](#将nps安装到系统)
-    * [流量数据持久化](#流量数据持久化)
-    * [系统信息显示](#系统信息显示)
-    * [自定义客户端连接密钥](#自定义客户端连接密钥)
-    * [关闭公钥访问](#关闭公钥访问)
-    * [关闭web管理](#关闭web管理)
-    * [服务端多用户登陆](#服务端多用户登陆)
-    * [用户注册功能](#用户注册功能)
-    * [监听指定ip](#监听指定ip)
-    * [代理到服务端本地](#代理到服务端本地)
-* [客户端](#客户端)
-    * [客户端启动](#客户端启动)
-        * [无配置文件模式](#无配置文件模式)
-        * [配置文件模式](#配置文件模式)
-    * [配置文件说明](#配置文件说明)
-        * [全局配置](#全局配置)
-        * [域名代理](#域名代理)
-        * [tcp隧道](#tcp隧道模式)
-        * [udp隧道](#udp隧道模式)
-        * [http正向代理](#http代理模式)
-        * [socks5代理](#socks5代理模式)
-        * [私密代理](#私密代理模式)
-        * [p2p服务](#p2p代理)
-        * [文件访问代理](#文件访问模式)
-    * [断线重连](#断线重连)
-    * [nat类型检测](#nat类型检测)
-    * [状态检查](#状态检查)
-    * [重载配置文件](#重载配置文件)
-    * [通过代理连接nps](#通过代理连接nps)
-    * [群晖支持](#群晖支持)
+### Server start
 
-* [相关功能](#相关功能)
-   * [缓存支持](#缓存支持)
-   * [数据压缩支持](#数据压缩支持)
-   * [站点密码保护](#站点保护)
-   * [加密传输](#加密传输)
-   * [host修改](#host修改)
-   * [自定义header](#自定义header)
-   * [自定义404页面](#404页面配置)
-   * [流量限制](#流量限制)
-   * [带宽限制](#带宽限制)
-   * [负载均衡](#负载均衡)
-   * [端口白名单](#端口白名单)
-   * [端口范围映射](#端口范围映射)
-   * [端口范围映射到其他机器](#端口范围映射到其他机器)
-   * [守护进程](#守护进程)
-   * [KCP协议支持](#KCP协议支持)
-   * [域名泛解析](#域名泛解析)
-   * [URL路由](#URL路由)
-   * [限制ip访问](#限制ip访问)
-   * [客户端最大连接数限制](#客户端最大连接数)
-   * [客户端最大隧道数限制](#客户端最大隧道数限制)
-   * [端口复用](#端口复用)
-   * [多路复用](#多路复用)
-   * [环境变量渲染](#环境变量渲染)
-   * [健康检查](#健康检查)
-   * [日志输出](#日志输出)
-* [相关说明](#相关说明)
-   * [流量统计](#流量统计)
-   * [当前客户端带宽](#当前客户端带宽)
-   * [热更新支持](#热更新支持)
-   * [获取用户真实ip](#获取用户真实ip)
-   * [客户端地址显示](#客户端地址显示)
-   * [客户端与服务端版本对比](#客户端与服务端版本对比)
-   * [Linux系统限制](#Linux系统限制)
-* [webAPI](#webAPI)
-* [贡献](#贡献)
-* [支持nps发展](#捐赠)
-* [交流群](#交流群)
+After downloading the server compressed package, unzip it, and then enter the unzipped folder.
 
+- execute installation command
 
+For linux、darwin ```sudo ./nps install```
 
-## 安装
+For windows, run cmd as administrator and enter the installation directory ```nps.exe install```
 
-### release安装
-> [releases](https://github.com/cnlh/nps/releases)
+- start up
 
-下载对应的系统版本即可,服务端和客户端是单独的
+For linux、darwin ```sudo nps start```
 
-### 源码安装
-- 安装源码
-> go get -u github.com/cnlh/nps...
-- 编译
-> go build cmd/nps/nps.go
+For windows, run cmd as administrator and enter the program directory ```nps.exe start```
 
-> go build cmd/npc/npc.go
+```After installation, the windows configuration file is located at C:\Program Files\nps, linux or darwin is located at /etc/nps```
 
-### docker安装
-> [server](https://hub.docker.com/r/ffdfgdfg/nps)
-> [client](https://hub.docker.com/r/ffdfgdfg/npc)
+**If you don't find it started successfully, you can check the log (Windows log files are located in the current running directory, linux and darwin are located in /var/log/nps.log).**
 
-## 使用示例
+- Access server IP:web service port (default is 8080).
+- Login with username and password (default is admin/123, must be modified when officially used).
+- Create a client.
 
-### 统一准备工作(必做)
-- 开启服务端,假设公网服务器ip为1.1.1.1,配置文件中`bridge_port`为8284,配置文件中`web_port`为8080
-- 访问1.1.1.1:8080
-- 在客户端管理中创建一个客户端,记录下验证密钥
-- 内网客户端运行(windows使用cmd运行加.exe)
+### Client connection
+- Click the + sign in front of the client in web management and copy the startup command.
+- Execute the startup command, Linux can be executed directly, Windows will replace ./npc with npc.exe and execute it with cmd.
 
-```shell
-./npc -server=1.1.1.1:8284 -vkey=客户端的密钥
-```
-**注意:运行服务端后,请确保能从客户端设备上正常访问配置文件中所配置的`bridge_port`端口,telnet,netcat这类的来检查**
 
-### 域名解析
+If you need to register to the system service, you can check [Register to the system service](https://ehang-io.github.io/nps/#/use?id=注册到系统服务)
 
-**适用范围:** 小程序开发、微信公众号开发、产品演示
+### Configuration
+- After the client connects, configure the corresponding penetration service in the web.
+- For more advanced usage, see [Complete Documentation](https://ehang-io.github.io/nps/)
 
-**假设场景:**
-- 有一个域名proxy.com,有一台公网机器ip为1.1.1.1
-- 两个内网开发站点127.0.0.1:81,127.0.0.1:82
-- 想通过(http|https://)a.proxy.com访问127.0.0.1:81,通过(http|https://)b.proxy.com访问127.0.0.1:82
-
-**使用步骤**
-- 将*.proxy.com解析到公网服务器1.1.1.1
-- 点击刚才创建的客户端的域名管理,添加两条规则规则:1、域名:`a.proxy.com`,内网目标:`127.0.0.1:81`,2、域名:`b.proxy.com`,内网目标:`127.0.0.1:82`
-
-现在访问(http|https://)`a.proxy.com`,`b.proxy.com`即可成功
-
-**https:** 如需使用https请进行相关配置,详见 [使用https](#使用https)
-
-### tcp隧道
-
-
-**适用范围:**  ssh、远程桌面等tcp连接场景
-
-**假设场景:**
- 想通过访问公网服务器1.1.1.1的8001端口,连接内网机器10.1.50.101的22端口,实现ssh连接
-
-**使用步骤**
-- 在刚才创建的客户端隧道管理中添加一条tcp隧道,填写监听的端口(8001)、内网目标ip和目标端口(10.1.50.101:22),保存。
-- 访问公网服务器ip(1.1.1.1),填写的监听端口(8001),相当于访问内网ip(10.1.50.101):目标端口(22),例如:`ssh -p 8001 root@1.1.1.1`
-
-### udp隧道
-
-**适用范围:**  内网dns解析等udp连接场景
-
-**假设场景:**
-内网有一台dns(10.1.50.102:53),在非内网环境下想使用该dns,公网服务器为1.1.1.1
-
-**使用步骤**
-- 在刚才创建的客户端的隧道管理中添加一条udp隧道,填写监听的端口(53)、内网目标ip和目标端口(10.1.50.102:53),保存。
-- 修改需要使用的dns地址为1.1.1.1,则相当于使用10.1.50.102作为dns服务器
-
-### socks5代理
-
-
-**适用范围:**  在外网环境下如同使用vpn一样访问内网设备或者资源
-
-**假设场景:**
-想将公网服务器1.1.1.1的8003端口作为socks5代理,达到访问内网任意设备或者资源的效果
-
-**使用步骤**
-- 在刚才创建的客户端隧道管理中添加一条socks5代理,填写监听的端口(8003),保存。
-- 在外网环境的本机配置socks5代理(例如使用proxifier进行全局代理),ip为公网服务器ip(1.1.1.1),端口为填写的监听端口(8003),即可畅享内网了
-
-**注意**
-经过socks5代理,当收到socks5数据包时socket已经是accept状态。表现是扫描端口全open,建立连接后短时间关闭。若想同内网表现一致,建议远程连接一台设备。
-
-### http正向代理
-
-**适用范围:**  在外网环境下使用http正向代理访问内网站点
-
-**假设场景:**
-想将公网服务器1.1.1.1的8004端口作为http代理,访问内网网站
-
-**使用步骤**
-
-- 在刚才创建的客户端隧道管理中添加一条http代理,填写监听的端口(8004),保存。
-- 在外网环境的本机配置http代理,ip为公网服务器ip(1.1.1.1),端口为填写的监听端口(8004),即可访问了
-
-### 私密代理
-
-**适用范围:**  无需占用多余的端口、安全性要求较高可以防止其他人连接的tcp服务,例如ssh。
-
-**假设场景:**
-无需新增多的端口实现访问内网服务器10.1.50.2的22端口
-
-**使用步骤**
-- 在刚才创建的客户端中添加一条私密代理,并设置唯一密钥secrettest和内网目标10.1.50.2:22
-- 在需要连接ssh的机器上以执行命令
-
-```
-./npc -server=1.1.1.1:8284 -vkey=vkey -type=tcp -password=secrettest -local_type=secret
-```
-如需指定本地端口可加参数`-local_port=xx`,默认为2000
-
-**注意:** password为web管理上添加的唯一密钥,具体命令可查看web管理上的命令提示
-
-假设10.1.50.2用户名为root,现在执行`ssh -p 2000 root@1.1.1.1`即可访问ssh
-
-
-### p2p服务
-
-**适用范围:**  大流量传输场景,流量不经过公网服务器,但是由于p2p穿透和nat类型关系较大,不保证100%成功,支持大部分nat类型。[nat类型检测](#nat类型检测)
-
-**假设场景:**
-内网1机器ip为10.1.50.2    内网2机器2 ip为10.2.50.2
-
-想通过访问内网1机器1的2000端口---->访问到内网2机器3 10.2.50.3的22端口
-
-**使用步骤**
-- 在`nps.conf`中设置`p2p_ip`(nps服务器ip)和`p2p_port`(nps服务器udp端口)
-- 在刚才刚才创建的客户端中添加一条p2p代理,并设置唯一密钥p2pssh
-- 在机器1执行命令
-
-```
-./npc -server=1.1.1.1:8284 -vkey=123 -password=p2pssh -target=10.2.50.3:22
-```
-如需指定本地端口可加参数`-local_port=xx`,默认为2000
-
-**注意:** password为web管理上添加的唯一密钥,具体命令可查看web管理上的命令提示
-
-假设机器3用户名为root,现在在机器1上执行`ssh -p 2000 root@127.0.0.1`即可访问机器2的ssh
-
-
-
-## web管理
-
-![image](https://github.com/cnlh/nps/blob/master/image/web2.png?raw=true)
-### 介绍
-
-可在网页上配置和管理各个tcp、udp隧道、内网站点代理,http、https解析等,功能强大,操作方便。
-
-
-**提示:使用web模式时,服务端执行文件必须在项目根目录,否则无法正确加载配置文件**
-
-### 启动
-
-
-#### 服务端测试
-```shell
- ./nps test
-```
-如有错误请及时修改配置文件,无错误可继续进行下去
-#### 服务端启动
-```shell
- ./nps start
-```
-**如果无需daemon运行或者打开后无法正常访问web管理,去掉start查看日志运行即可**
-
-#### web管理
-
-进入web界面,公网ip:web界面端口(默认8080),密码默认为123
-
-进入web管理界面,有详细的说明
-
-#### 服务端配置文件重载
-如果是daemon启动
-```shell
- ./nps reload
-```
-**说明:** 仅支持部分配置重载,例如`allow_user_login` `auth_crypt_key` `auth_key` `web_username` `web_password` 等,未来将支持更多
-
-
-#### 服务端停止或重启
-如果是daemon启动
-```shell
- ./nps stop|restart
-```
-
-### 服务端配置文件
-- /conf/nps.conf
-
-名称 | 含义
----|---
-web_port | web管理端口
-web_password | web界面管理密码
-web_username | web界面管理账号
-bridge_port  | 服务端客户端通信端口
-https_proxy_port | 域名代理https代理监听端口
-http_proxy_port | 域名代理http代理监听端口
-auth_key|web api密钥
-bridge_type|客户端与服务端连接方式kcp或tcp
-public_vkey|客户端以配置文件模式启动时的密钥,设置为空表示关闭客户端配置文件连接模式
-ip_limit|是否限制ip访问,true或false或忽略
-flow_store_interval|服务端流量数据持久化间隔,单位分钟,忽略表示不持久化
-log_level|日志输出级别
-auth_crypt_key | 获取服务端authKey时的aes加密密钥,16位
-p2p_ip| 服务端Ip,使用p2p模式必填
-p2p_port|p2p模式开启的udp端口
-
-### 使用https
-
-**方式一:** 类似于nginx实现https的处理
-
-在配置文件中将https_proxy_port设置为443或者其他你想配置的端口,和在web中对应域名编辑中设置对应的证书路径,将`https_just_proxy`设置为false,然后就和http代理一样了
-
-**此外:** 可以在`nps.conf`中设置一个默认的https配置,当遇到未在web中设置https证书的域名解析时,将自动使用默认证书,另还有一种情况就是对于某些请求的clienthello不携带sni扩展信息,nps也将自动使用默认证书
-
-
-**方式二:** 在内网对应服务器上设置https
-
-在`nps.conf`中将`https_just_proxy`设置为true,并且打开`https_proxy_port`端口,然后nps将直接转发https请求到内网服务器上,由内网服务器进行https处理
-
-### 与nginx配合
-
-有时候我们还需要在云服务器上运行nginx来保证静态文件缓存等,本代理可和nginx配合使用,在配置文件中将httpProxyPort设置为非80端口,并在nginx中配置代理,例如httpProxyPort为8024时
-```
-server {
-    listen 80;
-    server_name *.proxy.com;
-    location / {
-        proxy_set_header Host  $http_host;
-        proxy_pass http://127.0.0.1:8024;
-    }
-}
-```
-如需使用https也可在nginx监听443端口并配置ssl,并将本代理的httpsProxyPort设置为空关闭https即可,例如httpProxyPort为8024时
-
-```
-server {
-    listen 443;
-    server_name *.proxy.com;
-    ssl on;
-    ssl_certificate  certificate.crt;
-    ssl_certificate_key private.key;
-    ssl_session_timeout 5m;
-    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
-    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
-    ssl_prefer_server_ciphers on;
-    location / {
-        proxy_set_header Host  $http_host;
-        proxy_pass http://127.0.0.1:8024;
-    }
-}
-```
-### 关闭代理
-
-如需关闭http代理可在配置文件中将http_proxy_port设置为空,如需关闭https代理可在配置文件中将https_proxy_port设置为空。
-
-### 将nps安装到系统
-如果需要长期并且方便的运行nps服务端,可将nps安装到操作系统中,可执行命令
-
-```
-(./nps|nps.exe) install
-```
-安装成功后,对于linux,darwin,将会把配置文件和静态文件放置于/etc/nps/,并将可执行文件nps复制到/usr/bin/nps或者/usr/local/bin/nps,安装成功后可在任何位置执行,同时也会添加systemd配置。
-
-```
-sudo systemctl enable|disable|start|stop|restart|status nps
-```
-systemd,带有开机自启,自动重启配置,当进程结束后15秒会启动,日志输出至/var/log/nps/nps.log。
-建议采用此方式启动,能够捕获panic信息,便于排查问题。
-
-```
-nps test|start|stop|restart|status
-```
-对于windows系统,将会把配置文件和静态文件放置于C:\Program Files\nps,安装成功后可将可执行文件nps.exe复制到任何位置执行
-
-```
-nps.exe test|start|stop|restart|status
-```
-
-### 流量数据持久化
-服务端支持将流量数据持久化,默认情况下是关闭的,如果有需求可以设置`nps.conf`中的`flow_store_interval`参数,单位为分钟
-
-**注意:** nps不会持久化通过公钥连接的客户端
-### 系统信息显示
-nps服务端支持在web上显示和统计服务器的相关信息,但默认一些统计图表是关闭的,如需开启请在`nps.conf`中设置`system_info_display=true`
-
-### 自定义客户端连接密钥
-web上可以自定义客户端连接的密钥,但是必须具有唯一性
-### 关闭公钥访问
-可以将`nps.conf`中的`public_vkey`设置为空或者删除
-
-### 关闭web管理
-可以将`nps.conf`中的`web_port`设置为空或者删除
-
-### 服务端多用户登陆
-如果将`nps.conf`中的`allow_user_login`设置为true,服务端web将支持多用户登陆,登陆用户名为user,默认密码为每个客户端的验证密钥,登陆后可以进入客户端编辑修改web登陆的用户名和密码,默认该功能是关闭的。
-
-### 用户注册功能
-nps服务端支持用户注册功能,可将`nps.conf`中的`allow_user_register`设置为true,开启后登陆页将会有有注册功能,
-
-### 监听指定ip
-
-nps支持每个隧道监听不同的服务端端口,在`nps.conf`中设置`allow_multi_ip=true`后,可在web中控制,或者npc配置文件中(可忽略,默认为0.0.0.0)
-```ini
-server_ip=xxx
-```
-### 代理到服务端本地
-在使用nps监听80或者443端口时,默认是将所有的请求都会转发到内网上,但有时候我们的nps服务器的上一些服务也需要使用这两个端口,nps提供类似于`nginx` `proxy_pass` 的功能,支持将代理到服务器本地,该功能支持域名解析,tcp、udp隧道,默认关闭。
-
-**即:** 假设在nps的vps服务器上有一个服务使用5000端口,这时候nps占用了80端口和443,我们想能使用一个域名通过http(s)访问到5000的服务。
-
-**使用方式:** 在`nps.conf`中设置`allow_local_proxy=true`,然后在web上设置想转发的隧道或者域名然后选择转发到本地选项即可成功。
-## 客户端
-
-### 客户端启动
-#### 无配置文件模式
-此模式的各种配置在服务端web管理中完成,客户端除运行一条命令外无需任何其他设置
-```
- ./npc -server=ip:port -vkey=web界面中显示的密钥
-```
-#### 配置文件模式
-此模式使用nps的公钥或者客户端私钥验证,各种配置在客户端完成,同时服务端web也可以进行管理
-```
- ./npc -config=npc配置文件路径
-```
-可自行添加systemd service,例如:`npc.service`
-```
-[Unit]
-Description=npc - convenient proxy server client
-Documentation=https://github.com/cnlh/nps/
-After=network-online.target remote-fs.target nss-lookup.target
-Wants=network-online.target
-
-[Service]
-Type=simple
-KillMode=process
-Restart=always
-RestartSec=15s
-StandardOutput=append:/var/log/nps/npc.log
-ExecStartPre=/bin/echo 'Starting npc'
-ExecStopPost=/bin/echo 'Stopping npc'
-ExecStart=/absolutely path to/npc -server=ip:port -vkey=web界面中显示的密钥
-
-[Install]
-WantedBy=multi-user.target
-```
-#### 配置文件说明
-[示例配置文件](https://github.com/cnlh/nps/tree/master/conf/npc.conf)
-##### 全局配置
-```ini
-[common]
-server_addr=1.1.1.1:8284
-conn_type=tcp
-vkey=123
-username=111
-password=222
-compress=true
-crypt=true
-rate_limit=10000
-flow_limit=100
-remark=test
-max_conn=10
-```
-项 | 含义
----|---
-server_addr | 服务端ip:port
-conn_type | 与服务端通信模式(tcp或kcp)
-vkey|服务端配置文件中的密钥(非web)
-username|socks5或http(s)密码保护用户名(可忽略)
-password|socks5或http(s)密码保护密码(可忽略)
-compress|是否压缩传输(true或false或忽略)
-crypt|是否加密传输(true或false或忽略)
-rate_limit|速度限制,可忽略
-flow_limit|流量限制,可忽略
-remark|客户端备注,可忽略
-max_conn|最大连接数,可忽略
-##### 域名代理
-
-```ini
-[common]
-server_addr=1.1.1.1:8284
-vkey=123
-[web1]
-host=a.proxy.com
-target_addr=127.0.0.1:8080,127.0.0.1:8082
-host_change=www.proxy.com
-header_set_proxy=nps
-```
-项 | 含义
----|---
-web1 | 备注
-host | 域名(http|https都可解析)
-target_addr|内网目标,负载均衡时多个目标,逗号隔开
-host_change|请求host修改
-header_xxx|请求header修改或添加,header_proxy表示添加header proxy:nps
-
-##### tcp隧道模式
-
-```ini
-[common]
-server_addr=1.1.1.1:8284
-vkey=123
-[tcp]
-mode=tcp
-target_addr=127.0.0.1:8080
-server_port=9001
-```
-项 | 含义
----|---
-mode | tcp
-server_port | 在服务端的代理端口
-tartget_addr|内网目标
-
-##### udp隧道模式
-
-```ini
-[common]
-server_addr=1.1.1.1:8284
-vkey=123
-[udp]
-mode=udp
-target_addr=127.0.0.1:8080
-server_port=9002
-```
-项 | 含义
----|---
-mode | udp
-server_port | 在服务端的代理端口
-target_addr|内网目标
-##### http代理模式
-
-```ini
-[common]
-server_addr=1.1.1.1:8284
-vkey=123
-[http]
-mode=httpProxy
-server_port=9003
-```
-项 | 含义
----|---
-mode | httpProxy
-server_port | 在服务端的代理端口
-##### socks5代理模式
-
-```ini
-[common]
-server_addr=1.1.1.1:8284
-vkey=123
-[socks5]
-mode=socks5
-server_port=9004
-multi_account=multi_account.conf
-```
-项 | 含义
----|---
-mode | socks5
-server_port | 在服务端的代理端口
-multi_account | socks5多账号配置文件(可选),配置后使用basic_username和basic_password无法通过认证
-##### 私密代理模式
-
-```ini
-[common]
-server_addr=1.1.1.1:8284
-vkey=123
-[secret_ssh]
-mode=secret
-password=ssh2
-target_addr=10.1.50.2:22
-```
-项 | 含义
----|---
-mode | secret
-password | 唯一密钥
-target_addr|内网目标
-
-##### p2p代理模式
-
-```ini
-[common]
-server_addr=1.1.1.1:8284
-vkey=123
-[p2p_ssh]
-mode=p2p
-password=ssh2
-target_addr=10.1.50.2:22
-```
-项 | 含义
----|---
-mode | p2p
-password | 唯一密钥
-target_addr|内网目标
-
-
-##### 文件访问模式
-利用nps提供一个公网可访问的本地文件服务,此模式仅客户端使用配置文件模式方可启动
-
-```ini
-[common]
-server_addr=1.1.1.1:8284
-vkey=123
-[file]
-mode=file
-server_port=9100
-local_path=/tmp/
-strip_pre=/web/
-````
-
-项 | 含义
----|---
-mode | file
-server_port | 服务端开启的端口
-local_path|本地文件目录
-strip_pre|前缀
-
-对于`strip_pre`,访问公网`ip:9100/web/`相当于访问`/tmp/`目录
-
-#### 断线重连
-```ini
-[common]
-auto_reconnection=true
-```
-#### nat类型检测
-```
- ./npc nat
-```
-如果p2p双方都是Symmetric Nat,肯定不能成功,其他组合都有较大成功率。
-#### 状态检查
-```
- ./npc status -config=npc配置文件路径
-```
-#### 重载配置文件
-```
- ./npc restart -config=npc配置文件路径
-```
-
-#### 通过代理连接nps
-有时候运行npc的内网机器无法直接访问外网,此时可以可以通过socks5代理连接nps
-
-对于配置文件方式启动,设置
-```ini
-[common]
-proxy_url=socks5://111:222@127.0.0.1:8024
-```
-对于无配置文件模式,加上参数
-
-```
--proxy=socks5://111:222@127.0.0.1:8024
-```
-支持socks5和http两种模式
-
-即socks5://username:password@ip:port
-
-或http://username:password@ip:port
-
-#### 群晖支持
-可在releases中下载spk群晖套件,例如`npc_x64-6.1_0.19.0-1.spk`
-## 相关功能
-
-### 缓存支持
-对于web站点来说,一些静态文件往往消耗更大的流量,且在内网穿透中,静态文件还需到客户端获取一次,这将导致更大的流量消耗。nps在域名解析代理中支持对静态文件进行缓存。
-
-即假设一个站点有a.css,nps将只需从npc客户端读取一次该文件,然后把该文件的内容放在内存中,下一次将不再对npc客户端进行请求而直接返回内存中的对应内容。该功能默认是关闭的,如需开启请在`nps.conf`中设置`http_cache=true`,并设置`http_cache_length`(缓存文件的个数,消耗内存,不宜过大,0表示不限制个数)
-
-### 数据压缩支持
-
-由于是内网穿透,内网客户端与服务端之间的隧道存在大量的数据交换,为节省流量,加快传输速度,由此本程序支持SNNAPY形式的压缩。
-
-
-- 所有模式均支持数据压缩
-- 在web管理或客户端配置文件中设置
-
-
-### 加密传输
-
-如果公司内网防火墙对外网访问进行了流量识别与屏蔽,例如禁止了ssh协议等,通过设置 配置文件,将服务端与客户端之间的通信内容加密传输,将会有效防止流量被拦截。
-- nps使用tls加密,所以一定要保留conf目录下的密钥文件,同时也可以自行生成
-- 在web管理或客户端配置文件中设置
-
-
-
-### 站点保护
-域名代理模式所有客户端共用一个http服务端口,在知道域名后任何人都可访问,一些开发或者测试环境需要保密,所以可以设置用户名和密码,nps将通过 Http Basic Auth 来保护,访问时需要输入正确的用户名和密码。
-
-
-- 在web管理或客户端配置文件中设置
-
-### host修改
-
-由于内网站点需要的host可能与公网域名不一致,域名代理支持host修改功能,即修改request的header中的host字段。
-
-**使用方法:在web管理中设置**
-
-### 自定义header
-
-支持对header进行新增或者修改,以配合服务的需要
-
-### 404页面配置
-支持域名解析模式的自定义404页面,修改/web/static/page/error.html中内容即可,暂不支持静态文件等内容
-
-### 流量限制
-
-支持客户端级流量限制,当该客户端入口流量与出口流量达到设定的总量后会拒绝服务
-,域名代理会返回404页面,其他代理会拒绝连接,使用该功能需要在`nps.conf`中设置`allow_flow_limit`,默认是关闭的。
-
-### 带宽限制
-
-支持客户端级带宽限制,带宽计算方式为入口和出口总和,权重均衡,使用该功能需要在`nps.conf`中设置`allow_rate_limit`,默认是关闭的。
-
-### 负载均衡
-本代理支持域名解析模式和tcp代理的负载均衡,在web域名添加或者编辑中内网目标分行填写多个目标即可实现轮训级别的负载均衡
-
-### 端口白名单
-为了防止服务端上的端口被滥用,可在nps.conf中配置allow_ports限制可开启的端口,忽略或者不填表示端口不受限制,格式:
-
-```ini
-allow_ports=9001-9009,10001,11000-12000
-```
-
-### 端口范围映射
-当客户端以配置文件的方式启动时,可以将本地的端口进行范围映射,仅支持tcp和udp模式,例如:
-
-```ini
-[tcp]
-mode=tcp
-server_port=9001-9009,10001,11000-12000
-target_port=8001-8009,10002,13000-14000
-```
-
-逗号分隔,可单个或者范围,注意上下端口的对应关系,无法一一对应将不能成功
-### 端口范围映射到其他机器
-```ini
-[tcp]
-mode=tcp
-server_port=9001-9009,10001,11000-12000
-target_port=8001-8009,10002,13000-14000
-target_ip=10.1.50.2
-```
-填写target_ip后则表示映射的该地址机器的端口,忽略则便是映射本地127.0.0.1,仅范围映射时有效
-### 守护进程
-本代理支持守护进程,使用示例如下,服务端客户端所有模式通用,支持linux,darwin,windows。
-```
-./(nps|npc) start|stop|restart|status 若有其他参数可加其他参数
-```
-```
-(nps|npc).exe start|stop|restart|status 若有其他参数可加其他参数
-```
-### KCP协议支持
-
-KCP 是一个快速可靠协议,能以比 TCP浪费10%-20%的带宽的代价,换取平均延迟降低 30%-40%,在弱网环境下对性能能有一定的提升。可在nps.conf中修改`bridge_type`为kcp
-,设置后本代理将开启udp端口(`bridge_port`)
-
-注意:当服务端为kcp时,客户端连接时也需要使用相同配置,无配置文件模式加上参数type=kcp,配置文件模式在配置文件中设置tp=kcp
-
-### 域名泛解析
-支持域名泛解析,例如将host设置为*.proxy.com,a.proxy.com、b.proxy.com等都将解析到同一目标,在web管理中或客户端配置文件中将host设置为此格式即可。
-
-### URL路由
-本代理支持根据URL将同一域名转发到不同的内网服务器,可在web中或客户端配置文件中设置,此参数也可忽略,例如在客户端配置文件中
-
-```ini
-[web1]
-host=a.proxy.com
-target_addr=127.0.0.1:7001
-location=/test
-[web2]
-host=a.proxy.com
-target_addr=127.0.0.1:7002
-location=/static
-```
-对于`a.proxy.com/test`将转发到`web1`,对于`a.proxy.com/static`将转发到`web2`
-
-### 限制ip访问
-如果将一些危险性高的端口例如ssh端口暴露在公网上,可能会带来一些风险,本代理支持限制ip访问。
-
-**使用方法:** 在配置文件nps.conf中设置`ip_limit`=true,设置后仅通过注册的ip方可访问。
-
-**ip注册**:
-
-**方式一:**
-在需要访问的机器上,运行客户端
-
-```
-./npc register -server=ip:port -vkey=公钥或客户端密钥 time=2
-```
-
-time为有效小时数,例如time=2,在当前时间后的两小时内,本机公网ip都可以访问nps代理.
-
-**方式二:**
-此外nps的web登陆也可提供验证的功能,成功登陆nps web admin后将自动为登陆的ip注册两小时的允许访问权限。
-
-
-**注意:** 本机公网ip并不是一成不变的,请自行注意有效期的设置,同时同一网络下,多人也可能是在公用同一个公网ip。
-### 客户端最大连接数
-为防止恶意大量长连接,影响服务端程序的稳定性,可以在web或客户端配置文件中为每个客户端设置最大连接数。该功能针对`socks5`、`http正向代理`、`域名代理`、`tcp代理`、`udp代理`、`私密代理`生效,使用该功能需要在`nps.conf`中设置`allow_connection_num_limit=true`,默认是关闭的。
-
-### 客户端最大隧道数限制
-nps支持对客户端的隧道数量进行限制,该功能默认是关闭的,如需开启,请在`nps.conf`中设置`allow_tunnel_num_limit=true`。
-### 端口复用
-在一些严格的网络环境中,对端口的个数等限制较大,nps支持强大端口复用功能。将`bridge_port`、 `http_proxy_port`、 `https_proxy_port` 、`web_port`都设置为同一端口,也能正常使用。
-
-- 使用时将需要复用的端口设置为与`bridge_port`一致即可,将自动识别。
-- 如需将web管理的端口也复用,需要配置`web_host`也就是一个二级域名以便区分
-
-### 多路复用
-
-nps主要通信默认基于多路复用,无需开启。
-
-多路复用基于TCP滑动窗口原理设计,动态计算延迟以及带宽来算出应该往网络管道中打入的流量。
-由于主要通信大多采用TCP协议,并无法探测其实时丢包情况,对于产生丢包重传的情况,采用较大的宽容度,
-5分钟的等待时间,超时将会关闭当前隧道连接并重新建立,这将会抛弃当前所有的连接。
-在Linux上,可以通过调节内核参数来适应不同应用场景。
-
-对于需求大带宽又有一定的丢包的场景,可以保持默认参数不变,尽可能少抛弃连接
-高并发下可根据[Linux系统限制](#Linux系统限制) 调整
-
-对于延迟敏感而又有一定丢包的场景,可以适当调整TCP重传次数
-`tcp_syn_retries`, `tcp_retries1`, `tcp_retries2`
-高并发同上
-nps会在系统主动关闭连接的时候拿到报错,进而重新建立隧道连接
-
-### 环境变量渲染
-npc支持环境变量渲染以适应在某些特殊场景下的要求。
-
-**在无配置文件启动模式下:**
-设置环境变量
-```
-export NPC_SERVER_ADDR=1.1.1.1:8284
-export NPC_SERVER_VKEY=xxxxx
-```
-直接执行./npc即可运行
-
-**在配置文件启动模式下:**
-```ini
-[common]
-server_addr={{.NPC_SERVER_ADDR}}
-conn_type=tcp
-vkey={{.NPC_SERVER_VKEY}}
-auto_reconnection=true
-[web]
-host={{.NPC_WEB_HOST}}
-target_addr={{.NPC_WEB_TARGET}}
-```
-在配置文件中填入相应的环境变量名称,npc将自动进行渲染配置文件替换环境变量
-
-### 健康检查
-
-当客户端以配置文件模式启动时,支持多节点的健康检查。配置示例如下
-
-```ini
-[health_check_test1]
-health_check_timeout=1
-health_check_max_failed=3
-health_check_interval=1
-health_http_url=/
-health_check_type=http
-health_check_target=127.0.0.1:8083,127.0.0.1:8082
-
-[health_check_test2]
-health_check_timeout=1
-health_check_max_failed=3
-health_check_interval=1
-health_check_type=tcp
-health_check_target=127.0.0.1:8083,127.0.0.1:8082
-```
-**health关键词必须在开头存在**
-
-第一种是http模式,也就是以get的方式请求目标+url,返回状态码为200表示成功
-
-第一种是tcp模式,也就是以tcp的方式与目标建立连接,能成功建立连接表示成功
-
-如果失败次数超过`health_check_max_failed`,nps则会移除该npc下的所有该目标,如果失败后目标重新上线,nps将自动将目标重新加入。
-
-项 | 含义
----|---
-health_check_timeout |  健康检查超时时间
-health_check_max_failed |  健康检查允许失败次数
-health_check_interval |  健康检查间隔
-health_check_type |  健康检查类型
-health_check_target |  健康检查目标,多个以逗号(,)分隔
-health_check_type |  健康检查类型
-health_http_url |  健康检查url,仅http模式适用
-
-### 日志输出
-
-#### 日志输出级别
-
-**对于npc:**
-```
--log_level=0~7 -log_path=npc.log
-```
-```
-LevelEmergency->0  LevelAlert->1
-
-LevelCritical->2 LevelError->3
-
-LevelWarning->4 LevelNotice->5
-
-LevelInformational->6 LevelDebug->7
-```
-默认为全输出,级别为0到7
-
-**对于nps:**
-
-在`nps.conf`中设置相关配置即可
-
-## 相关说明
-
-### 获取用户真实ip
-
-在域名代理模式中,可以通过request请求 header 中的 X-Forwarded-For 和 X-Real-IP 来获取用户真实 IP。
-
-**本代理前会在每一个http(s)请求中添加了这两个 header。**
-
-### 热更新支持
-对于绝大多数配置,在web管理中的修改将实时使用,无需重启客户端或者服务端
-
-### 客户端地址显示
-在web管理中将显示客户端的连接地址
-
-### 流量统计
-可统计显示每个代理使用的流量,由于压缩和加密等原因,会和实际环境中的略有差异
-
-### 当前客户端带宽
-可统计每个客户端当前的带宽,可能和实际有一定差异,仅供参考。
-
-### 客户端与服务端版本对比
-为了程序正常运行,客户端与服务端的核心版本必须一致,否则将导致客户端无法成功连接致服务端。
-
-### Linux系统限制
-默认情况下linux对连接数量有限制,对于性能好的机器完全可以调整内核参数以处理更多的连接。
-`tcp_max_syn_backlog` `somaxconn`
-酌情调整参数,增强网络性能
-
-## webAPI
-
-### webAPI验证说明
-- 采用auth_key的验证方式
-- 在提交的每个请求后面附带两个参数,`auth_key` 和`timestamp`
-
-```
-auth_key的生成方式为:md5(配置文件中的auth_key+当前时间戳)
-```
-
-```
-timestamp为当前时间戳
-```
-```
-curl --request POST \
-  --url http://127.0.0.1:8080/client/list \
-  --data 'auth_key=2a0000d9229e7dbcf79dd0f5e04bb084&timestamp=1553045344&start=0&limit=10'
-```
-**注意:** 为保证安全,时间戳的有效范围为20秒内,所以每次提交请求必须重新生成。
-
-### 获取服务端时间
-由于服务端与api请求的客户端时间差异不能太大,所以提供了一个可以获取服务端时间的接口
-
-```
-POST /auth/gettime
-```
-
-### 获取服务端authKey
-
-如果想获取authKey,服务端提供获取authKey的接口
-
-```
-POST /auth/getauthkey
-```
-将返回加密后的authKey,采用aes cbc加密,请使用与服务端配置文件中cryptKey相同的密钥进行解密
-
-**注意:** nps配置文件中`auth_crypt_key`需为16位
-- 解密密钥长度128
-- 偏移量与密钥相同
-- 补码方式pkcs5padding
-- 解密串编码方式 十六进制
-
-### 详细文档
-- **此文档近期可能更新较慢,建议自行抓包**
-
-为方便第三方扩展,在web模式下可利用webAPI进行相关操作,详情见
-[webAPI文档](https://github.com/cnlh/nps/wiki/webAPI%E6%96%87%E6%A1%A3)
-
-## 贡献
-#### **欢迎参与到制作docker、图标、文档翻译等工作**
-- 如果遇到bug可以直接提交至dev分支
-- 使用遇到问题可以通过issues反馈
-- 项目处于开发阶段,还有很多待完善的地方,如果可以贡献代码,请提交 PR 至 dev 分支
-- 如果有新的功能特性反馈,可以通过issues或者qq群反馈
-
-## 捐助
-如果您觉得nps对你有帮助,欢迎给予我们一定捐助,也是帮助nps更好的发展。
-
-### 支付宝
-![image](https://github.com/cnlh/nps/blob/master/image/donation_zfb.png?raw=true)
-### 微信
-![image](https://github.com/cnlh/nps/blob/master/image/donation_wx.png?raw=true)
-## 交流群
-
-![二维码.jpeg](https://i.loli.net/2019/02/15/5c66c32a42074.jpeg)
+## Contribution
+- If you encounter a bug, you can submit it to the dev branch directly.
+- If you encounter a problem, you can feedback through the issue.
+- The project is under development, and there is still a lot of room for improvement. If you can contribute code, please submit PR to the dev branch.
+- If there is feedback on new features, you can feedback via issues or qq group.

+ 80 - 0
README_zh.md

@@ -0,0 +1,80 @@
+
+# nps
+![](https://img.shields.io/github/stars/ehang-io/nps.svg)   ![](https://img.shields.io/github/forks/ehang-io/nps.svg)
+[![Gitter](https://badges.gitter.im/cnlh-nps/community.svg)](https://gitter.im/cnlh-nps/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
+[![Build Status](https://travis-ci.org/ehang-io/nps.svg?branch=master)](https://travis-ci.org/ehang-io/nps)
+![GitHub All Releases](https://img.shields.io/github/downloads/ehang-io/nps/total)
+
+[README](https://github.com/ehang-io/nps/blob/master/README.md)|[中文文档](https://github.com/ehang-io/nps/blob/master/README_zh.md)
+
+nps是一款轻量级、高性能、功能强大的**内网穿透**代理服务器。目前支持**tcp、udp流量转发**,可支持任何**tcp、udp**上层协议(访问内网网站、本地支付接口调试、ssh访问、远程桌面,内网dns解析等等……),此外还**支持内网http代理、内网socks5代理**、**p2p等**,并带有功能强大的web管理端。
+
+
+## 背景
+![image](https://github.com/ehang-io/nps/blob/master/image/web.png?raw=true)
+
+1. 做微信公众号开发、小程序开发等----> 域名代理模式
+
+2. 想在外网通过ssh连接内网的机器,做云服务器到内网服务器端口的映射,----> tcp代理模式
+
+3. 在非内网环境下使用内网dns,或者需要通过udp访问内网机器等----> udp代理模式
+
+4. 在外网使用HTTP代理访问内网站点----> http代理模式
+
+5. 搭建一个内网穿透ss,在外网如同使用内网vpn一样访问内网资源或者设备----> socks5代理模式
+## 特点
+- 协议支持全面,兼容几乎所有常用协议,例如tcp、udp、http(s)、socks5、p2p、http代理...
+- 全平台兼容(linux、windows、macos、群辉等),支持一键安装为系统服务
+- 控制全面,同时支持服务端和客户端控制
+- https集成,支持将后端代理和web服务转成https,同时支持多证书
+- 操作简单,只需简单的配置即可在web ui上完成其余操作
+- 展示信息全面,流量、系统信息、即时带宽、客户端版本等
+- 扩展功能强大,该有的都有了(缓存、压缩、加密、流量限制、带宽限制、端口复用等等)
+- 域名解析具备自定义header、404页面配置、host修改、站点保护、URL路由、泛解析等功能
+- 服务端支持多用户和用户注册功能
+
+**没找到你想要的功能?不要紧,点击[进入文档](https://ehang-io.github.io/nps)查找吧**
+## 快速开始
+
+### 安装
+> [releases](https://github.com/ehang-io/nps/releases)
+
+下载对应的系统版本即可,服务端和客户端是单独的
+
+### 服务端启动
+下载完服务器压缩包后,解压,然后进入解压后的文件夹
+
+- 执行安装命令
+
+对于linux|darwin ```sudo ./nps install```
+
+对于windows,管理员身份运行cmd,进入安装目录 ```nps.exe install```
+
+- 启动
+
+对于linux|darwin ```sudo nps start```
+
+对于windows,管理员身份运行cmd,进入程序目录 ```nps.exe start```
+
+```安装后windows配置文件位于 C:\Program Files\nps,linux和darwin位于/etc/nps```
+
+**如果发现没有启动成功,可以查看日志(Windows日志文件位于当前运行目录下,linux和darwin位于/var/log/nps.log)**
+- 访问服务端ip:web服务端口(默认为8080)
+- 使用用户名和密码登陆(默认admin/123,正式使用一定要更改)
+- 创建客户端
+
+### 客户端连接
+- 点击web管理中客户端前的+号,复制启动命令
+- 执行启动命令,linux直接执行即可,windows将./npc换成npc.exe用cmd执行
+
+如果需要注册到系统服务可查看[注册到系统服务](https://ehang-io.github.io/nps/#/use?id=注册到系统服务)
+
+### 配置
+- 客户端连接后,在web中配置对应穿透服务即可
+- 更多高级用法见[完整文档](https://ehang-io.github.io/nps/)
+
+## 贡献
+- 如果遇到bug可以直接提交至dev分支
+- 使用遇到问题可以通过issues反馈
+- 项目处于开发阶段,还有很多待完善的地方,如果可以贡献代码,请提交 PR 至 dev 分支
+- 如果有新的功能特性反馈,可以通过issues或者qq群反馈

+ 31 - 20
bridge/bridge.go

@@ -11,30 +11,32 @@ import (
 	"sync"
 	"time"
 
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/conn"
+	"ehang.io/nps/lib/crypt"
+	"ehang.io/nps/lib/file"
+	"ehang.io/nps/lib/mux"
+	"ehang.io/nps/lib/version"
+	"ehang.io/nps/server/connection"
+	"ehang.io/nps/server/tool"
 	"github.com/astaxie/beego"
 	"github.com/astaxie/beego/logs"
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/conn"
-	"github.com/cnlh/nps/lib/crypt"
-	"github.com/cnlh/nps/lib/file"
-	"github.com/cnlh/nps/lib/mux"
-	"github.com/cnlh/nps/lib/version"
-	"github.com/cnlh/nps/server/connection"
-	"github.com/cnlh/nps/server/tool"
 )
 
 type Client struct {
 	tunnel    *mux.Mux
 	signal    *conn.Conn
 	file      *mux.Mux
+	Version   string
 	retryTime int // it will be add 1 when ping not ok until to 3 will close the client
 }
 
-func NewClient(t, f *mux.Mux, s *conn.Conn) *Client {
+func NewClient(t, f *mux.Mux, s *conn.Conn, vs string) *Client {
 	return &Client{
-		signal: s,
-		tunnel: t,
-		file:   f,
+		signal:  s,
+		tunnel:  t,
+		file:    f,
+		Version: vs,
 	}
 }
 
@@ -166,16 +168,23 @@ func (s *Bridge) cliProcess(c *conn.Conn) {
 		return
 	}
 	//version check
-	if b, err := c.GetShortContent(32); err != nil || string(b) != crypt.Md5(version.GetVersion()) {
+	if b, err := c.GetShortLenContent(); err != nil || string(b) != version.GetVersion() {
 		logs.Info("The client %s version does not match", c.Conn.RemoteAddr())
 		c.Close()
 		return
 	}
+	//version get
+	var vs []byte
+	var err error
+	if vs, err = c.GetShortLenContent(); err != nil {
+		logs.Info("get client %s version error", err.Error())
+		c.Close()
+		return
+	}
 	//write server version to client
 	c.Write([]byte(crypt.Md5(version.GetVersion())))
 	c.SetReadDeadlineBySecond(5)
 	var buf []byte
-	var err error
 	//get vKey from client
 	if buf, err = c.GetShortContent(32); err != nil {
 		c.Close()
@@ -191,7 +200,7 @@ func (s *Bridge) cliProcess(c *conn.Conn) {
 		s.verifySuccess(c)
 	}
 	if flag, err := c.ReadFlag(); err == nil {
-		s.typeDeal(flag, c, id)
+		s.typeDeal(flag, c, id, string(vs))
 	} else {
 		logs.Warn(err, flag)
 	}
@@ -214,7 +223,7 @@ func (s *Bridge) DelClient(id int) {
 }
 
 //use different
-func (s *Bridge) typeDeal(typeVal string, c *conn.Conn, id int) {
+func (s *Bridge) typeDeal(typeVal string, c *conn.Conn, id int, vs string) {
 	isPub := file.GetDb().IsPubClient(id)
 	switch typeVal {
 	case common.WORK_MAIN:
@@ -223,17 +232,18 @@ func (s *Bridge) typeDeal(typeVal string, c *conn.Conn, id int) {
 			return
 		}
 		//the vKey connect by another ,close the client of before
-		if v, ok := s.Client.LoadOrStore(id, NewClient(nil, nil, c)); ok {
+		if v, ok := s.Client.LoadOrStore(id, NewClient(nil, nil, c, vs)); ok {
 			if v.(*Client).signal != nil {
 				v.(*Client).signal.WriteClose()
 			}
 			v.(*Client).signal = c
+			v.(*Client).Version = vs
 		}
 		go s.GetHealthFromClient(id, c)
 		logs.Info("clientId %d connection succeeded, address:%s ", id, c.Conn.RemoteAddr())
 	case common.WORK_CHAN:
 		muxConn := mux.NewMux(c.Conn, s.tunnelType)
-		if v, ok := s.Client.LoadOrStore(id, NewClient(muxConn, nil, nil)); ok {
+		if v, ok := s.Client.LoadOrStore(id, NewClient(muxConn, nil, nil, vs)); ok {
 			v.(*Client).tunnel = muxConn
 		}
 	case common.WORK_CONFIG:
@@ -254,7 +264,7 @@ func (s *Bridge) typeDeal(typeVal string, c *conn.Conn, id int) {
 		}
 	case common.WORK_FILE:
 		muxConn := mux.NewMux(c.Conn, s.tunnelType)
-		if v, ok := s.Client.LoadOrStore(id, NewClient(nil, muxConn, nil)); ok {
+		if v, ok := s.Client.LoadOrStore(id, NewClient(nil, muxConn, nil, vs)); ok {
 			v.(*Client).file = muxConn
 		}
 	case common.WORK_P2P:
@@ -358,6 +368,7 @@ func (s *Bridge) SendLinkInfo(clientId int, link *conn.Link, t *file.Tunnel) (ta
 
 func (s *Bridge) ping() {
 	ticker := time.NewTicker(time.Second * 5)
+	defer ticker.Stop()
 	for {
 		select {
 		case <-ticker.C:
@@ -434,7 +445,7 @@ loop:
 				}
 				c.WriteAddOk()
 				c.Write([]byte(client.VerifyKey))
-				s.Client.Store(client.Id, NewClient(nil, nil, nil))
+				s.Client.Store(client.Id, NewClient(nil, nil, nil, ""))
 			}
 		case common.NEW_HOST:
 			h, err := c.GetHostInfo()

+ 39 - 0
build.android.sh

@@ -0,0 +1,39 @@
+#/bin/bash
+#sudo apt-get install libgl1-mesa-dev xorg-dev
+#go get github.com/ffdfgdfg/fyne-cross
+#fyne-cross --targets=linux/amd64,windows/amd64,darwin/amd64 gui/npc/npc.go
+
+cd /go
+apt-get install libegl1-mesa-dev libgles2-mesa-dev libx11-dev -y
+#go get -u fyne.io/fyne/cmd/fyne fyne.io/fyne
+mkdir -p /go/src/fyne.io
+cd src/fyne.io
+git clone https://github.com/fyne-io/fyne.git
+cd fyne
+git checkout v1.2.0
+go install -v ./cmd/fyne
+#fyne package -os android fyne.io/fyne/cmd/hello
+echo "fyne install success"
+mkdir -p /go/src/ehang.io/nps
+cp -R /app/* /go/src/ehang.io/nps
+cd /go/src/ehang.io/nps
+#go get -u fyne.io/fyne fyne.io/fyne/cmd/fyne
+rm cmd/npc/sdk.go
+#go get -u ./...
+#go mod tidy
+#rm -rf /go/src/golang.org/x/mobile
+echo "tidy success"
+cd /go/src/ehang.io/nps
+go mod vendor
+cd vendor
+cp -R * /go/src
+cd ..
+rm -rf vendor
+#rm -rf ~/.cache/*
+echo "vendor success"
+cd gui/npc
+#rm -rf /go/src/golang.org/x/mobile
+#go get -u fyne.io/fyne/cmd/fyne@v1.2.0
+#export ANDROID_NDK_HOME=/usr/local/android_sdk/ndk-bundle
+fyne package -appID org.nps.client -os android -icon ../../docs/logo.png
+mv npc.apk /app/android_client.apk

+ 179 - 0
build.sh

@@ -0,0 +1,179 @@
+#/bash/sh
+export VERSION=0.26.0
+
+sudo apt-get install gcc-mingw-w64-i686
+env GOOS=windows GOARCH=386 CGO_ENABLED=1 CC=i686-w64-mingw32-gcc go build -ldflags "-s -w -extldflags -static -extldflags -static" -buildmode=c-shared -o npc_sdk.dll cmd/npc/sdk.go
+tar -czvf npc_sdk.tar.gz npc_sdk.dll npc_sdk.h
+
+wget https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz
+tar -xvf upx-3.95-amd64_linux.tar.xz
+cp upx-3.95-amd64_linux/upx ./
+
+CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -extldflags -static -extldflags -static"  ./cmd/npc/npc.go
+
+tar -czvf linux_amd64_client.tar.gz npc conf/npc.conf conf/multi_account.conf
+
+CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/npc/npc.go
+
+tar -czvf linux_386_client.tar.gz npc conf/npc.conf conf/multi_account.conf
+
+CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/npc/npc.go
+
+tar -czvf freebsd_386_client.tar.gz npc conf/npc.conf conf/multi_account.conf
+
+CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/npc/npc.go
+
+tar -czvf freebsd_amd64_client.tar.gz npc conf/npc.conf conf/multi_account.conf
+
+CGO_ENABLED=0 GOOS=freebsd GOARCH=arm go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/npc/npc.go
+
+tar -czvf freebsd_arm_client.tar.gz npc conf/npc.conf conf/multi_account.conf
+
+CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/npc/npc.go
+
+tar -czvf linux_arm_v7_client.tar.gz npc conf/npc.conf conf/multi_account.conf
+
+CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/npc/npc.go
+
+tar -czvf linux_arm_v6_client.tar.gz npc conf/npc.conf conf/multi_account.conf
+
+CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=5 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/npc/npc.go
+
+tar -czvf linux_arm_v5_client.tar.gz npc conf/npc.conf conf/multi_account.conf
+
+
+CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/npc/npc.go
+
+tar -czvf linux_arm64_client.tar.gz npc conf/npc.conf conf/multi_account.conf
+
+
+CGO_ENABLED=0 GOOS=linux GOARCH=mips64 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/npc/npc.go
+
+tar -czvf linux_mips64_client.tar.gz npc conf/npc.conf conf/multi_account.conf
+
+
+CGO_ENABLED=0 GOOS=linux GOARCH=mips64le go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/npc/npc.go
+
+tar -czvf linux_mips64le_client.tar.gz npc conf/npc.conf conf/multi_account.conf
+
+
+CGO_ENABLED=0 GOOS=linux GOARCH=mipsle go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/npc/npc.go
+
+tar -czvf linux_mipsle_client.tar.gz npc conf/npc.conf conf/multi_account.conf
+
+
+CGO_ENABLED=0 GOOS=linux GOARCH=mips go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/npc/npc.go
+
+tar -czvf linux_mips_client.tar.gz npc conf/npc.conf conf/multi_account.conf
+
+
+CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/npc/npc.go
+
+tar -czvf windows_386_client.tar.gz npc.exe conf/npc.conf conf/multi_account.conf
+
+
+CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/npc/npc.go
+
+tar -czvf windows_amd64_client.tar.gz npc.exe conf/npc.conf conf/multi_account.conf
+
+
+CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/npc/npc.go
+
+tar -czvf darwin_amd64_client.tar.gz npc conf/npc.conf conf/multi_account.conf
+
+CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/nps/nps.go
+
+tar -czvf linux_amd64_server.tar.gz conf/nps.conf conf/tasks.json conf/clients.json conf/hosts.json conf/server.key  conf/server.pem web/views web/static nps
+
+CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/nps/nps.go
+
+tar -czvf linux_386_server.tar.gz conf/nps.conf conf/tasks.json conf/clients.json conf/hosts.json conf/server.key  conf/server.pem web/views web/static nps
+
+CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=5 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/nps/nps.go
+
+tar -czvf linux_arm_v5_server.tar.gz conf/nps.conf conf/tasks.json conf/clients.json conf/hosts.json conf/server.key  conf/server.pem web/views web/static nps
+
+CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/nps/nps.go
+
+tar -czvf linux_arm_v6_server.tar.gz conf/nps.conf conf/tasks.json conf/clients.json conf/hosts.json conf/server.key  conf/server.pem web/views web/static nps
+
+CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/nps/nps.go
+
+tar -czvf linux_arm_v7_server.tar.gz conf/nps.conf conf/tasks.json conf/clients.json conf/hosts.json conf/server.key  conf/server.pem web/views web/static nps
+
+
+CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/nps/nps.go
+
+tar -czvf linux_arm64_server.tar.gz conf/nps.conf conf/tasks.json conf/clients.json conf/hosts.json conf/server.key  conf/server.pem web/views web/static nps
+
+
+CGO_ENABLED=0 GOOS=freebsd GOARCH=arm go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/nps/nps.go
+
+tar -czvf freebsd_arm_server.tar.gz conf/nps.conf conf/tasks.json conf/clients.json conf/hosts.json conf/server.key  conf/server.pem web/views web/static nps
+
+
+CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/nps/nps.go
+
+tar -czvf freebsd_386_server.tar.gz conf/nps.conf conf/tasks.json conf/clients.json conf/hosts.json conf/server.key  conf/server.pem web/views web/static nps
+
+
+CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/nps/nps.go
+
+tar -czvf freebsd_amd64_server.tar.gz conf/nps.conf conf/tasks.json conf/clients.json conf/hosts.json conf/server.key  conf/server.pem web/views web/static nps
+
+
+
+CGO_ENABLED=0 GOOS=linux GOARCH=mips go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/nps/nps.go
+
+tar -czvf linux_mips_server.tar.gz conf/nps.conf conf/tasks.json conf/clients.json conf/hosts.json conf/server.key  conf/server.pem web/views web/static nps
+
+
+CGO_ENABLED=0 GOOS=linux GOARCH=mips64 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/nps/nps.go
+
+tar -czvf linux_mips64_server.tar.gz conf/nps.conf conf/tasks.json conf/clients.json conf/hosts.json conf/server.key  conf/server.pem web/views web/static nps
+
+
+CGO_ENABLED=0 GOOS=linux GOARCH=mips64le go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/nps/nps.go
+
+tar -czvf linux_mips64le_server.tar.gz conf/nps.conf conf/tasks.json conf/clients.json conf/hosts.json conf/server.key  conf/server.pem web/views web/static nps
+
+
+CGO_ENABLED=0 GOOS=linux GOARCH=mipsle go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/nps/nps.go
+
+tar -czvf linux_mipsle_server.tar.gz conf/nps.conf conf/tasks.json conf/clients.json conf/hosts.json conf/server.key  conf/server.pem web/views web/static nps
+
+
+
+CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/nps/nps.go
+
+tar -czvf darwin_amd64_server.tar.gz conf/nps.conf conf/tasks.json conf/clients.json conf/hosts.json conf/server.key  conf/server.pem web/views web/static nps
+
+
+CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/nps/nps.go
+
+tar -czvf windows_amd64_server.tar.gz conf/nps.conf conf/tasks.json conf/clients.json conf/hosts.json conf/server.key  conf/server.pem web/views web/static nps.exe
+
+
+CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags "-s -w -extldflags -static -extldflags -static" ./cmd/nps/nps.go
+
+tar -czvf windows_386_server.tar.gz conf/nps.conf conf/tasks.json conf/clients.json conf/hosts.json conf/server.key  conf/server.pem web/views web/static nps.exe
+
+curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
+sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
+sudo apt-get update
+sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce
+docker --version
+docker run --rm -i -w /app -v $(pwd):/app -e ANDROID_HOME=/usr/local/android_sdk ffdfgdfg/fyne-cross:android /app/build.android.sh
+git clone https://github.com/cnlh/spksrc.git ~/spksrc
+mkdir ~/spksrc/nps && cp -rf ./* ~/spksrc/nps/
+docker run -itd --name spksrc --env VERSION=$VERSION  -v ~/spksrc:/spksrc synocommunity/spksrc /bin/bash
+docker exec -it spksrc /bin/bash -c 'cd /spksrc && make setup && cd /spksrc/spk/npc && make'
+cp ~/spksrc/packages/npc_noarch-all_$VERSION-1.spk ./npc_syno.spk
+
+
+echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
+export DOCKER_CLI_EXPERIMENTAL=enabled
+docker run --rm --privileged docker/binfmt:66f9012c56a8316f9244ffd7622d7c21c1f6f28d
+docker buildx create --use --name mybuilder
+docker buildx build --tag ffdfgdfg/nps:$VERSION --tag ffdfgdfg/nps:latest --output type=image,push=true --file Dockerfile.nps --platform=linux/amd64,linux/arm64,linux/386,linux/arm .
+docker buildx build --tag ffdfgdfg/npc:$VERSION --tag ffdfgdfg/npc:latest --output type=image,push=true --file Dockerfile.npc --platform=linux/amd64,linux/arm64,linux/386,linux/arm .

+ 85 - 9
client/client.go

@@ -2,18 +2,20 @@ package client
 
 import (
 	"bufio"
+	"bytes"
 	"net"
 	"net/http"
 	"strconv"
 	"time"
 
 	"github.com/astaxie/beego/logs"
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/config"
-	"github.com/cnlh/nps/lib/conn"
-	"github.com/cnlh/nps/lib/crypt"
-	"github.com/cnlh/nps/lib/mux"
 	"github.com/xtaci/kcp-go"
+
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/config"
+	"ehang.io/nps/lib/conn"
+	"ehang.io/nps/lib/crypt"
+	"ehang.io/nps/lib/mux"
 )
 
 type TRPClient struct {
@@ -40,12 +42,20 @@ func NewRPClient(svraddr string, vKey string, bridgeConnType string, proxyUrl st
 	}
 }
 
+var NowStatus int
+var CloseClient bool
+
 //start
 func (s *TRPClient) Start() {
+	CloseClient = false
 retry:
+	if CloseClient {
+		return
+	}
+	NowStatus = 0
 	c, err := NewConn(s.bridgeConnType, s.vKey, s.svrAddr, common.WORK_MAIN, s.proxyUrl)
 	if err != nil {
-		logs.Error("The connection server failed and will be reconnected in five seconds")
+		logs.Error("The connection server failed and will be reconnected in five seconds, error", err.Error())
 		time.Sleep(time.Second * 5)
 		goto retry
 	}
@@ -64,6 +74,7 @@ retry:
 	if s.cnf != nil && len(s.cnf.Healths) > 0 {
 		go heathCheck(s.cnf.Healths, s.signal)
 	}
+	NowStatus = 1
 	//msg connection, eg udp
 	s.handleMain()
 }
@@ -156,7 +167,7 @@ func (s *TRPClient) newChan() {
 
 func (s *TRPClient) handleChan(src net.Conn) {
 	lk, err := conn.NewConn(src).GetLinkInfo()
-	if err != nil {
+	if err != nil || lk == nil {
 		src.Close()
 		logs.Error("get connection info from server error ", err)
 		return
@@ -165,7 +176,7 @@ func (s *TRPClient) handleChan(src net.Conn) {
 	lk.Host = common.FormatAddress(lk.Host)
 	//if Conn type is http, read the request and log
 	if lk.ConnType == "http" {
-		if targetConn, err := net.Dial(common.CONN_TCP, lk.Host); err != nil {
+		if targetConn, err := net.DialTimeout(common.CONN_TCP, lk.Host, lk.Option.Timeout); err != nil {
 			logs.Warn("connect to %s error %s", lk.Host, err.Error())
 			src.Close()
 		} else {
@@ -188,8 +199,12 @@ func (s *TRPClient) handleChan(src net.Conn) {
 		}
 		return
 	}
+	if lk.ConnType == "udp5" {
+		logs.Trace("new %s connection with the goal of %s, remote address:%s", lk.ConnType, lk.Host, lk.RemoteAddr)
+		s.handleUdp(src)
+	}
 	//connect to target if conn type is tcp or udp
-	if targetConn, err := net.Dial(lk.ConnType, lk.Host); err != nil {
+	if targetConn, err := net.DialTimeout(lk.ConnType, lk.Host, lk.Option.Timeout); err != nil {
 		logs.Warn("connect to %s error %s", lk.Host, err.Error())
 		src.Close()
 	} else {
@@ -198,6 +213,65 @@ func (s *TRPClient) handleChan(src net.Conn) {
 	}
 }
 
+func (s *TRPClient) handleUdp(serverConn net.Conn) {
+	// bind a local udp port
+	local, err := net.ListenUDP("udp", nil)
+	defer local.Close()
+	defer serverConn.Close()
+	if err != nil {
+		logs.Error("bind local udp port error ", err.Error())
+		return
+	}
+	go func() {
+		defer serverConn.Close()
+		b := common.BufPoolUdp.Get().([]byte)
+		defer common.BufPoolUdp.Put(b)
+		for {
+			n, raddr, err := local.ReadFrom(b)
+			if err != nil {
+				logs.Error("read data from remote server error", err.Error())
+			}
+			buf := bytes.Buffer{}
+			dgram := common.NewUDPDatagram(common.NewUDPHeader(0, 0, common.ToSocksAddr(raddr)), b[:n])
+			dgram.Write(&buf)
+			b, err := conn.GetLenBytes(buf.Bytes())
+			if err != nil {
+				logs.Warn("get len bytes error", err.Error())
+				continue
+			}
+			if _, err := serverConn.Write(b); err != nil {
+				logs.Error("write data to remote  error", err.Error())
+				return
+			}
+		}
+	}()
+	b := common.BufPoolUdp.Get().([]byte)
+	defer common.BufPoolUdp.Put(b)
+	for {
+		n, err := serverConn.Read(b)
+		if err != nil {
+			logs.Error("read udp data from server error ", err.Error())
+			return
+		}
+
+		udpData, err := common.ReadUDPDatagram(bytes.NewReader(b[:n]))
+		if err != nil {
+			logs.Error("unpack data error", err.Error())
+			return
+		}
+		raddr, err := net.ResolveUDPAddr("udp", udpData.Header.Addr.String())
+		if err != nil {
+			logs.Error("build remote addr err", err.Error())
+			continue // drop silently
+		}
+		_, err = local.WriteTo(udpData.Data, raddr)
+		if err != nil {
+			logs.Error("write data to remote ", raddr.String(), "error", err.Error())
+			return
+		}
+	}
+}
+
 // Whether the monitor channel is closed
 func (s *TRPClient) ping() {
 	s.ticker = time.NewTicker(time.Second * 5)
@@ -214,6 +288,8 @@ loop:
 }
 
 func (s *TRPClient) Close() {
+	CloseClient = true
+	NowStatus = 0
 	if s.tunnel != nil {
 		s.tunnel.Close()
 	}

+ 0 - 75
client/client_test.go

@@ -1,75 +0,0 @@
-package client
-
-import (
-	"net"
-	"sync"
-	"testing"
-
-	"github.com/cnlh/nps/lib/common"
-	conn2 "github.com/cnlh/nps/lib/conn"
-	"github.com/cnlh/nps/lib/file"
-)
-
-func TestConfig(t *testing.T) {
-	conn, err := net.Dial("tcp", "127.0.0.1:8284")
-	if err != nil {
-		t.Fail()
-	}
-	c := conn2.NewConn(conn)
-	c.SetAlive("tcp")
-	if _, err := c.Write([]byte(common.Getverifyval("123"))); err != nil {
-		t.Fail()
-	}
-	c.WriteConfig()
-	config := &file.Config{
-		U:              "1",
-		P:              "2",
-		Compress:       "snappy",
-		Crypt:          true,
-		CompressEncode: 0,
-		CompressDecode: 0,
-	}
-	host := &file.Host{
-		Host:         "a.o.com",
-		Target:       "127.0.0.1:8080",
-		HeaderChange: "",
-		HostChange:   "",
-		Flow:         nil,
-		Client:       nil,
-		Remark:       "111",
-		NowIndex:     0,
-		TargetArr:    nil,
-		NoStore:      false,
-		RWMutex:      sync.RWMutex{},
-	}
-	tunnel := &file.Tunnel{
-		Port:   9001,
-		Mode:   "tcp",
-		Target: "127.0.0.1:8082",
-		Remark: "333",
-	}
-	var b []byte
-	if b, err = c.ReadLen(16); err != nil {
-		t.Fail()
-	}
-	if _, err := c.SendConfigInfo(config); err != nil {
-		t.Fail()
-	}
-	if !c.GetAddStatus() {
-		t.Fail()
-	}
-	if _, err := c.SendHostInfo(host); err != nil {
-		t.Fail()
-	}
-	if !c.GetAddStatus() {
-		t.Fail()
-	}
-	if _, err := c.SendTaskInfo(tunnel); err != nil {
-		t.Fail()
-	}
-	if !c.GetAddStatus() {
-		t.Fail()
-	}
-	c.Close()
-	NewRPClient("127.0.0.1:8284", string(b), "tcp").Start()
-}

+ 14 - 10
client/control.go

@@ -19,12 +19,12 @@ import (
 	"strings"
 	"time"
 
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/config"
+	"ehang.io/nps/lib/conn"
+	"ehang.io/nps/lib/crypt"
+	"ehang.io/nps/lib/version"
 	"github.com/astaxie/beego/logs"
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/config"
-	"github.com/cnlh/nps/lib/conn"
-	"github.com/cnlh/nps/lib/crypt"
-	"github.com/cnlh/nps/lib/version"
 	"github.com/xtaci/kcp-go"
 	"golang.org/x/net/proxy"
 )
@@ -198,7 +198,7 @@ func NewConn(tp string, vkey string, server string, connType string, proxyUrl st
 					return nil, er
 				}
 				connection, err = n.Dial("tcp", server)
-			case "http":
+			default:
 				connection, err = NewHttpProxyConn(u, server)
 			}
 		} else {
@@ -220,7 +220,10 @@ func NewConn(tp string, vkey string, server string, connType string, proxyUrl st
 	if _, err := c.Write([]byte(common.CONN_TEST)); err != nil {
 		return nil, err
 	}
-	if _, err := c.Write([]byte(crypt.Md5(version.GetVersion()))); err != nil {
+	if err := c.WriteLenContent([]byte(version.GetVersion())); err != nil {
+		return nil, err
+	}
+	if err := c.WriteLenContent([]byte(version.VERSION)); err != nil {
 		return nil, err
 	}
 	b, err := c.GetShortContent(32)
@@ -238,8 +241,7 @@ func NewConn(tp string, vkey string, server string, connType string, proxyUrl st
 	if s, err := c.ReadFlag(); err != nil {
 		return nil, err
 	} else if s == common.VERIFY_EER {
-		logs.Error("Validation key %s incorrect", vkey)
-		os.Exit(0)
+		return nil, errors.New(fmt.Sprintf("Validation key %s incorrect", vkey))
 	}
 	if _, err := c.Write([]byte(connType)); err != nil {
 		return nil, err
@@ -259,7 +261,7 @@ func NewHttpProxyConn(url *url.URL, remoteAddr string) (net.Conn, error) {
 		Proto:  "HTTP/1.1",
 	}
 	password, _ := url.User.Password()
-	req.Header.Set("Proxy-Authorization", "Basic "+basicAuth(url.User.Username(), password))
+	req.Header.Set("Authorization", "Basic "+basicAuth(strings.Trim(url.User.Username(), " "), password))
 	b, err := httputil.DumpRequest(req, false)
 	if err != nil {
 		return nil, err
@@ -369,6 +371,7 @@ func sendP2PTestMsg(localConn *net.UDPConn, remoteAddr1, remoteAddr2, remoteAddr
 		}
 		logs.Trace("try send test packet to target %s", addr)
 		ticker := time.NewTicker(time.Millisecond * 500)
+		defer ticker.Stop()
 		for {
 			select {
 			case <-ticker.C:
@@ -394,6 +397,7 @@ func sendP2PTestMsg(localConn *net.UDPConn, remoteAddr1, remoteAddr2, remoteAddr
 						return
 					}
 					ticker := time.NewTicker(time.Second * 2)
+					defer ticker.Stop()
 					for {
 						select {
 						case <-ticker.C:

+ 3 - 3
client/health.go

@@ -7,10 +7,10 @@ import (
 	"strings"
 	"time"
 
+	"ehang.io/nps/lib/conn"
+	"ehang.io/nps/lib/file"
+	"ehang.io/nps/lib/sheap"
 	"github.com/astaxie/beego/logs"
-	"github.com/cnlh/nps/lib/conn"
-	"github.com/cnlh/nps/lib/file"
-	"github.com/cnlh/nps/lib/sheap"
 	"github.com/pkg/errors"
 )
 

+ 19 - 7
client/local.go

@@ -1,19 +1,21 @@
 package client
 
 import (
+	"errors"
 	"net"
 	"net/http"
+	"runtime"
 	"sync"
 	"time"
 
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/config"
+	"ehang.io/nps/lib/conn"
+	"ehang.io/nps/lib/crypt"
+	"ehang.io/nps/lib/file"
+	"ehang.io/nps/lib/mux"
+	"ehang.io/nps/server/proxy"
 	"github.com/astaxie/beego/logs"
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/config"
-	"github.com/cnlh/nps/lib/conn"
-	"github.com/cnlh/nps/lib/crypt"
-	"github.com/cnlh/nps/lib/file"
-	"github.com/cnlh/nps/lib/mux"
-	"github.com/cnlh/nps/server/proxy"
 	"github.com/xtaci/kcp-go"
 )
 
@@ -31,6 +33,14 @@ type p2pBridge struct {
 }
 
 func (p2pBridge *p2pBridge) SendLinkInfo(clientId int, link *conn.Link, t *file.Tunnel) (target net.Conn, err error) {
+	for i := 0; muxSession == nil; i++ {
+		if i >= 20 {
+			err = errors.New("p2pBridge:too many times to get muxSession")
+			logs.Error(err)
+			return
+		}
+		runtime.Gosched() // waiting for another goroutine establish the mux connection
+	}
 	nowConn, err := muxSession.NewConn()
 	if err != nil {
 		udpConn = nil
@@ -117,6 +127,7 @@ func StartLocalServer(l *config.LocalServer, config *config.CommonConfig) error
 
 func handleUdpMonitor(config *config.CommonConfig, l *config.LocalServer) {
 	ticker := time.NewTicker(time.Second * 1)
+	defer ticker.Stop()
 	for {
 		select {
 		case <-ticker.C:
@@ -157,6 +168,7 @@ func handleP2PVisitor(localTcpConn net.Conn, config *config.CommonConfig, l *con
 	if udpConn == nil {
 		logs.Notice("new conn, P2P can not penetrate successfully, traffic will be transferred through the server")
 		handleSecret(localTcpConn, config, l)
+		return
 	}
 	logs.Trace("start trying to connect with the server")
 	//TODO just support compress now because there is not tls file in client packages

+ 1 - 1
client/register.go

@@ -5,7 +5,7 @@ import (
 	"log"
 	"os"
 
-	"github.com/cnlh/nps/lib/common"
+	"ehang.io/nps/lib/common"
 )
 
 func RegisterLocalIp(server string, vKey string, tp string, proxyUrl string, hour int) {

+ 117 - 26
cmd/npc/npc.go

@@ -1,20 +1,21 @@
 package main
 
 import (
+	"ehang.io/nps/client"
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/config"
+	"ehang.io/nps/lib/file"
+	"ehang.io/nps/lib/install"
+	"ehang.io/nps/lib/version"
 	"flag"
 	"fmt"
+	"github.com/astaxie/beego/logs"
+	"github.com/ccding/go-stun/stun"
+	"github.com/kardianos/service"
 	"os"
+	"runtime"
 	"strings"
 	"time"
-
-	"github.com/astaxie/beego/logs"
-	"github.com/ccding/go-stun/stun"
-	"github.com/cnlh/nps/client"
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/config"
-	"github.com/cnlh/nps/lib/daemon"
-	"github.com/cnlh/nps/lib/file"
-	"github.com/cnlh/nps/lib/version"
 )
 
 var (
@@ -30,11 +31,60 @@ var (
 	password     = flag.String("password", "", "p2p password flag")
 	target       = flag.String("target", "", "p2p target")
 	localType    = flag.String("local_type", "p2p", "p2p target")
-	logPath      = flag.String("log_path", "npc.log", "npc log path")
+	logPath      = flag.String("log_path", "", "npc log path")
+	debug        = flag.Bool("debug", true, "npc debug")
 )
 
 func main() {
 	flag.Parse()
+	logs.Reset()
+	logs.EnableFuncCallDepth(true)
+	logs.SetLogFuncCallDepth(3)
+	if *logPath == "" {
+		*logPath = common.GetNpcLogPath()
+	}
+	if common.IsWindows() {
+		*logPath = strings.Replace(*logPath, "\\", "\\\\", -1)
+	}
+	if *debug {
+		logs.SetLogger(logs.AdapterConsole, `{"level":`+*logLevel+`,"color":true}`)
+	} else {
+		logs.SetLogger(logs.AdapterFile, `{"level":`+*logLevel+`,"filename":"`+*logPath+`","daily":false,"maxlines":100000,"color":true}`)
+	}
+
+	// init service
+	options := make(service.KeyValue)
+	options["Restart"] = "on-success"
+	options["SuccessExitStatus"] = "1 2 8 SIGKILL"
+	svcConfig := &service.Config{
+		Name:        "Npc",
+		DisplayName: "nps内网穿透客户端",
+		Description: "一款轻量级、功能强大的内网穿透代理服务器。支持tcp、udp流量转发,支持内网http代理、内网socks5代理,同时支持snappy压缩、站点保护、加密传输、多路复用、header修改等。支持web图形化管理,集成多用户模式。",
+		Option:      options,
+	}
+	if !common.IsWindows() {
+		svcConfig.Dependencies = []string{
+			"Requires=network.target",
+			"After=network-online.target syslog.target"}
+	}
+	for _, v := range os.Args[1:] {
+		switch v {
+		case "install", "start", "stop", "uninstall", "restart":
+			continue
+		}
+		if !strings.Contains(v, "-service=") && !strings.Contains(v, "-debug=") {
+			svcConfig.Arguments = append(svcConfig.Arguments, v)
+		}
+	}
+	svcConfig.Arguments = append(svcConfig.Arguments, "-debug=false")
+	prg := &npc{
+		exit: make(chan struct{}),
+	}
+	s, err := service.New(prg, svcConfig)
+	if err != nil {
+		logs.Error(err)
+		return
+	}
 	if len(os.Args) >= 2 {
 		switch os.Args[1] {
 		case "status":
@@ -45,6 +95,9 @@ func main() {
 		case "register":
 			flag.CommandLine.Parse(os.Args[2:])
 			client.RegisterLocalIp(*serverAddr, *verifyKey, *connType, *proxyUrl, *registerTime)
+		case "update":
+			install.UpdateNpc()
+			return
 		case "nat":
 			nat, host, err := stun.NewClient().Discover()
 			if err != nil || host == nil {
@@ -53,16 +106,47 @@ func main() {
 			}
 			fmt.Printf("nat type: %s \npublic address: %s\n", nat.String(), host.String())
 			os.Exit(0)
+		case "install", "start", "stop", "uninstall", "restart":
+			if os.Args[1] == "install" {
+				service.Control(s, "stop")
+				service.Control(s, "uninstall")
+				install.InstallNpc()
+			}
+			err := service.Control(s, os.Args[1])
+			if err != nil {
+				logs.Error("Valid actions: %q\n%s", service.ControlAction, err.Error())
+			}
+			return
 		}
 	}
-	daemon.InitDaemon("npc", common.GetRunPath(), common.GetTmpPath())
-	logs.EnableFuncCallDepth(true)
-	logs.SetLogFuncCallDepth(3)
-	if *logType == "stdout" {
-		logs.SetLogger(logs.AdapterConsole, `{"level":`+*logLevel+`,"color":true}`)
-	} else {
-		logs.SetLogger(logs.AdapterFile, `{"level":`+*logLevel+`,"filename":"`+*logPath+`","daily":false,"maxlines":100000,"color":true}`)
+	s.Run()
+}
+
+type npc struct {
+	exit chan struct{}
+}
+
+func (p *npc) Start(s service.Service) error {
+	go p.run()
+	return nil
+}
+func (p *npc) Stop(s service.Service) error {
+	close(p.exit)
+	if service.Interactive() {
+		os.Exit(0)
 	}
+	return nil
+}
+
+func (p *npc) run() error {
+	defer func() {
+		if err := recover(); err != nil {
+			const size = 64 << 10
+			buf := make([]byte, size)
+			buf = buf[:runtime.Stack(buf, false)]
+			logs.Warning("npc: panic serving %v: %v\n%s", err, string(buf))
+		}
+	}()
 	//p2p or secret command
 	if *password != "" {
 		commonConfig := new(config.CommonConfig)
@@ -76,8 +160,8 @@ func main() {
 		localServer.Port = *localPort
 		commonConfig.Client = new(file.Client)
 		commonConfig.Client.Cnf = new(file.Config)
-		client.StartLocalServer(localServer, commonConfig)
-		return
+		go client.StartLocalServer(localServer, commonConfig)
+		return nil
 	}
 	env := common.GetEnvMap()
 	if *serverAddr == "" {
@@ -88,15 +172,22 @@ func main() {
 	}
 	logs.Info("the version of client is %s, the core version of client is %s", version.VERSION, version.GetVersion())
 	if *verifyKey != "" && *serverAddr != "" && *configPath == "" {
-		for {
-			client.NewRPClient(*serverAddr, *verifyKey, *connType, *proxyUrl, nil).Start()
-			logs.Info("It will be reconnected in five seconds")
-			time.Sleep(time.Second * 5)
-		}
+		go func() {
+			for {
+				client.NewRPClient(*serverAddr, *verifyKey, *connType, *proxyUrl, nil).Start()
+				logs.Info("It will be reconnected in five seconds")
+				time.Sleep(time.Second * 5)
+			}
+		}()
 	} else {
 		if *configPath == "" {
-			*configPath = "npc.conf"
+			*configPath = "conf/npc.conf"
 		}
-		client.StartFromFile(*configPath)
+		go client.StartFromFile(*configPath)
+	}
+	select {
+	case <-p.exit:
+		logs.Warning("stop...")
 	}
+	return nil
 }

+ 51 - 0
cmd/npc/sdk.go

@@ -0,0 +1,51 @@
+package main
+
+import (
+	"C"
+	"ehang.io/nps/client"
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/version"
+	"github.com/astaxie/beego/logs"
+)
+
+var cl *client.TRPClient
+
+//export StartClientByVerifyKey
+func StartClientByVerifyKey(serverAddr, verifyKey, connType, proxyUrl *C.char) int {
+	logs.SetLogger("store")
+	if cl != nil {
+		cl.Close()
+	}
+	cl = client.NewRPClient(C.GoString(serverAddr), C.GoString(verifyKey), C.GoString(connType), C.GoString(proxyUrl), nil)
+	go func() {
+		cl.Start()
+		return
+	}()
+	return 1
+}
+
+//export GetClientStatus
+func GetClientStatus() int {
+	return client.NowStatus
+}
+
+//export CloseClient
+func CloseClient() {
+	if cl != nil {
+		cl.Close()
+	}
+}
+
+//export Version
+func Version() *C.char {
+	return C.CString(version.VERSION)
+}
+
+//export Logs
+func Logs() *C.char {
+	return C.CString(common.GetLogMsg())
+}
+
+func main() {
+	// Need a main function to make CGO compile package as C shared library
+}

+ 122 - 30
cmd/nps/nps.go

@@ -1,46 +1,38 @@
 package main
 
 import (
+	"ehang.io/nps/lib/install"
 	"flag"
 	"log"
 	"os"
 	"path/filepath"
+	"runtime"
+	"strings"
 
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/crypt"
+	"ehang.io/nps/lib/daemon"
+	"ehang.io/nps/lib/file"
+	"ehang.io/nps/lib/version"
+	"ehang.io/nps/server"
+	"ehang.io/nps/server/connection"
+	"ehang.io/nps/server/tool"
 	"github.com/astaxie/beego"
 	"github.com/astaxie/beego/logs"
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/crypt"
-	"github.com/cnlh/nps/lib/daemon"
-	"github.com/cnlh/nps/lib/file"
-	"github.com/cnlh/nps/lib/install"
-	"github.com/cnlh/nps/lib/version"
-	"github.com/cnlh/nps/server"
-	"github.com/cnlh/nps/server/connection"
-	"github.com/cnlh/nps/server/test"
-	"github.com/cnlh/nps/server/tool"
-	_ "github.com/cnlh/nps/web/routers"
+
+	"ehang.io/nps/web/routers"
+	"github.com/kardianos/service"
 )
 
 var (
-	level   string
-	logType = flag.String("log", "stdout", "Log output mode(stdout|file)")
+	level string
 )
 
 func main() {
 	flag.Parse()
-	beego.LoadAppConfig("ini", filepath.Join(common.GetRunPath(), "conf", "nps.conf"))
-	if len(os.Args) > 1 {
-		switch os.Args[1] {
-		case "test":
-			test.TestServerConfig()
-			log.Println("test ok, no error")
-			return
-		case "start", "restart", "stop", "status", "reload":
-			daemon.InitDaemon("nps", common.GetRunPath(), common.GetTmpPath())
-		case "install":
-			install.InstallNps()
-			return
-		}
+	// init log
+	if err := beego.LoadAppConfig("ini", filepath.Join(common.GetRunPath(), "conf", "nps.conf")); err != nil {
+		log.Fatalln("load config file error", err.Error())
 	}
 	if level = beego.AppConfig.String("log_level"); level == "" {
 		level = "7"
@@ -48,11 +40,106 @@ func main() {
 	logs.Reset()
 	logs.EnableFuncCallDepth(true)
 	logs.SetLogFuncCallDepth(3)
-	if *logType == "stdout" {
-		logs.SetLogger(logs.AdapterConsole, `{"level":`+level+`,"color":true}`)
+	logPath := beego.AppConfig.String("log_path")
+	if logPath == "" {
+		logPath = common.GetLogPath()
+	}
+	if common.IsWindows() {
+		logPath = strings.Replace(logPath, "\\", "\\\\", -1)
+	}
+	// init service
+	options := make(service.KeyValue)
+	options["Restart"] = "on-success"
+	options["SuccessExitStatus"] = "1 2 8 SIGKILL"
+	svcConfig := &service.Config{
+		Name:        "Nps",
+		DisplayName: "nps内网穿透代理服务器",
+		Description: "一款轻量级、功能强大的内网穿透代理服务器。支持tcp、udp流量转发,支持内网http代理、内网socks5代理,同时支持snappy压缩、站点保护、加密传输、多路复用、header修改等。支持web图形化管理,集成多用户模式。",
+		Option:      options,
+	}
+	svcConfig.Arguments = append(svcConfig.Arguments, "service")
+	if len(os.Args) > 1 && os.Args[1] == "service" {
+		logs.SetLogger(logs.AdapterFile, `{"level":`+level+`,"filename":"`+logPath+`","daily":false,"maxlines":100000,"color":true}`)
 	} else {
-		logs.SetLogger(logs.AdapterFile, `{"level":`+level+`,"filename":"`+beego.AppConfig.String("log_path")+`","daily":false,"maxlines":100000,"color":true}`)
+		logs.SetLogger(logs.AdapterConsole, `{"level":`+level+`,"color":true}`)
+	}
+	if !common.IsWindows() {
+		svcConfig.Dependencies = []string{
+			"Requires=network.target",
+			"After=network-online.target syslog.target"}
+	}
+	prg := &nps{}
+	prg.exit = make(chan struct{})
+	s, err := service.New(prg, svcConfig)
+	if err != nil {
+		logs.Error(err)
+		return
 	}
+	if len(os.Args) > 1 && os.Args[1] != "service" {
+		switch os.Args[1] {
+		case "reload":
+			daemon.InitDaemon("nps", common.GetRunPath(), common.GetTmpPath())
+			return
+		case "install":
+			// uninstall before
+			service.Control(s, "stop")
+			service.Control(s, "uninstall")
+
+			binPath := install.InstallNps()
+			svcConfig.Executable = binPath
+			s, err := service.New(prg, svcConfig)
+			if err != nil {
+				logs.Error(err)
+				return
+			}
+			err = service.Control(s, os.Args[1])
+			if err != nil {
+				logs.Error("Valid actions: %q\n%s", service.ControlAction, err.Error())
+			}
+			return
+		case "start", "restart", "stop", "uninstall":
+			err := service.Control(s, os.Args[1])
+			if err != nil {
+				logs.Error("Valid actions: %q\n%s", service.ControlAction, err.Error())
+			}
+			return
+		case "update":
+			install.UpdateNps()
+			return
+		default:
+			logs.Error("command is not support")
+			return
+		}
+	}
+	s.Run()
+}
+
+type nps struct {
+	exit chan struct{}
+}
+
+func (p *nps) Start(s service.Service) error {
+	p.run()
+	return nil
+}
+func (p *nps) Stop(s service.Service) error {
+	close(p.exit)
+	if service.Interactive() {
+		os.Exit(0)
+	}
+	return nil
+}
+
+func (p *nps) run() error {
+	defer func() {
+		if err := recover(); err != nil {
+			const size = 64 << 10
+			buf := make([]byte, size)
+			buf = buf[:runtime.Stack(buf, false)]
+			logs.Warning("nps: panic serving %v: %v\n%s", err, string(buf))
+		}
+	}()
+	routers.Init()
 	task := &file.Tunnel{
 		Mode: "webServer",
 	}
@@ -66,5 +153,10 @@ func main() {
 	crypt.InitTls(filepath.Join(common.GetRunPath(), "conf", "server.pem"), filepath.Join(common.GetRunPath(), "conf", "server.key"))
 	tool.InitAllowPort()
 	tool.StartSystemInfo()
-	server.StartNewServer(bridgePort, task, beego.AppConfig.String("bridge_type"))
+	go server.StartNewServer(bridgePort, task, beego.AppConfig.String("bridge_type"))
+	select {
+	case <-p.exit:
+		logs.Warning("stop...")
+	}
+	return nil
 }

+ 7 - 1
conf/nps.conf

@@ -26,7 +26,7 @@ public_vkey=123
 
 # log level LevelEmergency->0  LevelAlert->1 LevelCritical->2 LevelError->3 LevelWarning->4 LevelNotice->5 LevelInformational->6 LevelDebug->7
 log_level=7
-log_path=nps.log
+#log_path=nps.log
 
 #Whether to restrict IP access, true or false or ignore
 #ip_limit=true
@@ -41,6 +41,12 @@ web_username=admin
 web_password=123
 web_port = 8080
 web_ip=0.0.0.0
+web_base_url=
+web_open_ssl=false
+web_cert_file=conf/server.pem
+web_key_file=conf/server.key
+# if web under proxy use sub path. like http://host/nps need this.
+#web_base_url=/nps
 
 #Web API unauthenticated IP address(the len of auth_crypt_key must be 16)
 auth_key=test

+ 0 - 0
docs/.nojekyll


+ 21 - 0
docs/README.md

@@ -0,0 +1,21 @@
+# nps
+![](https://img.shields.io/github/stars/cnlh/nps.svg)   ![](https://img.shields.io/github/forks/cnlh/nps.svg)
+[![Gitter](https://badges.gitter.im/cnlh-nps/community.svg)](https://gitter.im/cnlh-nps/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
+[![Build Status](https://travis-ci.org/cnlh/nps.svg?branch=master)](https://travis-ci.org/cnlh/nps)
+
+nps是一款轻量级、高性能、功能强大的**内网穿透**代理服务器。目前支持**tcp、udp流量转发**,可支持任何**tcp、udp**上层协议(访问内网网站、本地支付接口调试、ssh访问、远程桌面,内网dns解析等等……),此外还**支持内网http代理、内网socks5代理**、**p2p等**,并带有功能强大的web管理端。
+
+
+## 背景
+![image](https://github.com/ehang-io/nps/blob/master/image/web.png?raw=true)
+
+1. 做微信公众号开发、小程序开发等----> 域名代理模式
+
+
+2. 想在外网通过ssh连接内网的机器,做云服务器到内网服务器端口的映射,----> tcp代理模式
+
+3. 在非内网环境下使用内网dns,或者需要通过udp访问内网机器等----> udp代理模式
+
+4. 在外网使用HTTP代理访问内网站点----> http代理模式
+
+5. 搭建一个内网穿透ss,在外网如同使用内网vpn一样访问内网资源或者设备----> socks5代理模式

+ 16 - 0
docs/_coverpage.md

@@ -0,0 +1,16 @@
+![logo](logo.svg)
+
+# NPS <small>0.26.0</small>
+
+> 一款轻量级、高性能、功能强大的内网穿透代理服务器
+
+- 几乎支持所有协议
+- 支持内网http代理、内网socks5代理、p2p等
+- 简洁但功能强大的WEB管理界面
+- 支持服务端、客户端同时控制
+- 扩展功能强大
+- 全平台兼容,一键注册为服务
+
+
+[GitHub](https://github.com/ehang-io/nps/)
+[开始使用](#nps)

+ 28 - 0
docs/_sidebar.md

@@ -0,0 +1,28 @@
+* 入门
+  * [安装](install.md)
+  * [启动](run.md)
+  * [使用示例](example.md)
+* 服务端
+  * [介绍](introduction.md)
+  * [使用](nps_use.md)
+  * [配置文件](server_config.md)
+  * [增强功能](nps_extend.md)
+
+* 客户端
+
+  * [基本使用](use.md)
+  * [增强功能](npc_extend.md)
+
+* 扩展
+
+  * [功能](feature.md)
+  * [说明](description.md)
+  * [web api](api.md)
+  * [sdk](npc_sdk.md)
+
+* 其他
+
+  * [贡献](contribute.md)
+  * [捐助](donate.md)
+  * [致谢](thanks.md)
+  * [交流](discuss.md)

+ 46 - 0
docs/api.md

@@ -0,0 +1,46 @@
+# web api
+## webAPI验证说明
+- 采用auth_key的验证方式
+- 在提交的每个请求后面附带两个参数,`auth_key` 和`timestamp`
+
+```
+auth_key的生成方式为:md5(配置文件中的auth_key+当前时间戳)
+```
+
+```
+timestamp为当前时间戳
+```
+```
+curl --request POST \
+  --url http://127.0.0.1:8080/client/list \
+  --data 'auth_key=2a0000d9229e7dbcf79dd0f5e04bb084&timestamp=1553045344&start=0&limit=10'
+```
+**注意:** 为保证安全,时间戳的有效范围为20秒内,所以每次提交请求必须重新生成。
+
+## 获取服务端时间
+由于服务端与api请求的客户端时间差异不能太大,所以提供了一个可以获取服务端时间的接口
+
+```
+POST /auth/gettime
+```
+
+## 获取服务端authKey
+
+如果想获取authKey,服务端提供获取authKey的接口
+
+```
+POST /auth/getauthkey
+```
+将返回加密后的authKey,采用aes cbc加密,请使用与服务端配置文件中cryptKey相同的密钥进行解密
+
+**注意:** nps配置文件中`auth_crypt_key`需为16位
+- 解密密钥长度128
+- 偏移量与密钥相同
+- 补码方式pkcs5padding
+- 解密串编码方式 十六进制
+
+## 详细文档
+- **此文档近期可能更新较慢,建议自行抓包**
+
+为方便第三方扩展,在web模式下可利用webAPI进行相关操作,详情见
+[webAPI文档](https://github.com/ehang-io/nps/wiki/webAPI%E6%96%87%E6%A1%A3)

+ 6 - 0
docs/contribute.md

@@ -0,0 +1,6 @@
+# 贡献
+
+- 如果遇到bug可以直接提交至dev分支
+- 使用遇到问题可以通过issues反馈
+- 项目处于开发阶段,还有很多待完善的地方,如果可以贡献代码,请提交 PR 至 dev 分支
+- 如果有新的功能特性反馈,可以通过issues或者qq群反馈

+ 32 - 0
docs/description.md

@@ -0,0 +1,32 @@
+# 说明
+## 获取用户真实ip
+
+在域名代理模式中,可以通过request请求 header 中的 X-Forwarded-For 和 X-Real-IP 来获取用户真实 IP。
+
+**本代理前会在每一个http(s)请求中添加了这两个 header。**
+
+## 热更新支持
+对于绝大多数配置,在web管理中的修改将实时使用,无需重启客户端或者服务端
+
+## web端保护
+在一分钟内,如果密码错误次数超过10次,该ip在一分钟内将不能再次登陆。
+
+## 客户端地址显示
+在web管理中将显示客户端的连接地址
+
+## 流量统计
+可统计显示每个代理使用的流量,由于压缩和加密等原因,会和实际环境中的略有差异
+
+## 当前客户端带宽
+可统计每个客户端当前的带宽,可能和实际有一定差异,仅供参考。
+
+## 客户端与服务端版本对比
+为了程序正常运行,客户端与服务端的核心版本必须一致,否则将导致客户端无法成功连接致服务端。
+
+## Linux系统限制
+默认情况下linux对连接数量有限制,对于性能好的机器完全可以调整内核参数以处理更多的连接。
+`tcp_max_syn_backlog` `somaxconn`
+酌情调整参数,增强网络性能
+
+## web管理保护
+当一个ip连续登陆失败次数超过10次,将在一分钟内禁止该ip再次尝试。

+ 3 - 0
docs/discuss.md

@@ -0,0 +1,3 @@
+# 交流群
+
+![二维码.jpeg](https://i.loli.net/2019/02/15/5c66c32a42074.jpeg)

+ 7 - 0
docs/donate.md

@@ -0,0 +1,7 @@
+# 捐助
+如果您觉得nps对你有帮助,欢迎给予我们一定捐助,也是帮助nps更好的发展。
+
+## 支付宝
+![image](https://github.com/ehang-io/nps/blob/master/image/donation_zfb.png?raw=true)
+## 微信
+![image](https://github.com/ehang-io/nps/blob/master/image/donation_wx.png?raw=true)

+ 121 - 0
docs/example.md

@@ -0,0 +1,121 @@
+# 使用示例
+## 统一准备工作(必做)
+- 开启服务端,假设公网服务器ip为1.1.1.1,配置文件中`bridge_port`为8284,配置文件中`web_port`为8080
+- 访问1.1.1.1:8080
+- 在客户端管理中创建一个客户端,记录下验证密钥
+- 内网客户端运行(windows使用cmd运行加.exe)
+
+```shell
+./npc -server=1.1.1.1:8284 -vkey=客户端的密钥
+```
+**注意:运行服务端后,请确保能从客户端设备上正常访问配置文件中所配置的`bridge_port`端口,telnet,netcat这类的来检查**
+
+## 域名解析
+
+**适用范围:** 小程序开发、微信公众号开发、产品演示
+
+**假设场景:**
+- 有一个域名proxy.com,有一台公网机器ip为1.1.1.1
+- 两个内网开发站点127.0.0.1:81,127.0.0.1:82
+- 想通过(http|https://)a.proxy.com访问127.0.0.1:81,通过(http|https://)b.proxy.com访问127.0.0.1:82
+
+**使用步骤**
+- 将*.proxy.com解析到公网服务器1.1.1.1
+- 点击刚才创建的客户端的域名管理,添加两条规则规则:1、域名:`a.proxy.com`,内网目标:`127.0.0.1:81`,2、域名:`b.proxy.com`,内网目标:`127.0.0.1:82`
+
+现在访问(http|https://)`a.proxy.com`,`b.proxy.com`即可成功
+
+**https:** 如需使用https请进行相关配置,详见 [使用https](/nps_extend?id=使用https)
+
+## tcp隧道
+
+
+**适用范围:**  ssh、远程桌面等tcp连接场景
+
+**假设场景:**
+ 想通过访问公网服务器1.1.1.1的8001端口,连接内网机器10.1.50.101的22端口,实现ssh连接
+
+**使用步骤**
+- 在刚才创建的客户端隧道管理中添加一条tcp隧道,填写监听的端口(8001)、内网目标ip和目标端口(10.1.50.101:22),保存。
+- 访问公网服务器ip(1.1.1.1),填写的监听端口(8001),相当于访问内网ip(10.1.50.101):目标端口(22),例如:`ssh -p 8001 root@1.1.1.1`
+
+## udp隧道
+
+**适用范围:**  内网dns解析等udp连接场景
+
+**假设场景:**
+内网有一台dns(10.1.50.102:53),在非内网环境下想使用该dns,公网服务器为1.1.1.1
+
+**使用步骤**
+- 在刚才创建的客户端的隧道管理中添加一条udp隧道,填写监听的端口(53)、内网目标ip和目标端口(10.1.50.102:53),保存。
+- 修改需要使用的dns地址为1.1.1.1,则相当于使用10.1.50.102作为dns服务器
+
+## socks5代理
+
+
+**适用范围:**  在外网环境下如同使用vpn一样访问内网设备或者资源
+
+**假设场景:**
+想将公网服务器1.1.1.1的8003端口作为socks5代理,达到访问内网任意设备或者资源的效果
+
+**使用步骤**
+- 在刚才创建的客户端隧道管理中添加一条socks5代理,填写监听的端口(8003),保存。
+- 在外网环境的本机配置socks5代理(例如使用proxifier进行全局代理),ip为公网服务器ip(1.1.1.1),端口为填写的监听端口(8003),即可畅享内网了
+
+**注意**
+经过socks5代理,当收到socks5数据包时socket已经是accept状态。表现是扫描端口全open,建立连接后短时间关闭。若想同内网表现一致,建议远程连接一台设备。
+
+## http正向代理
+
+**适用范围:**  在外网环境下使用http正向代理访问内网站点
+
+**假设场景:**
+想将公网服务器1.1.1.1的8004端口作为http代理,访问内网网站
+
+**使用步骤**
+
+- 在刚才创建的客户端隧道管理中添加一条http代理,填写监听的端口(8004),保存。
+- 在外网环境的本机配置http代理,ip为公网服务器ip(1.1.1.1),端口为填写的监听端口(8004),即可访问了
+
+## 私密代理
+
+**适用范围:**  无需占用多余的端口、安全性要求较高可以防止其他人连接的tcp服务,例如ssh。
+
+**假设场景:**
+无需新增多的端口实现访问内网服务器10.1.50.2的22端口
+
+**使用步骤**
+- 在刚才创建的客户端中添加一条私密代理,并设置唯一密钥secrettest和内网目标10.1.50.2:22
+- 在需要连接ssh的机器上以执行命令
+
+```
+./npc -server=1.1.1.1:8284 -vkey=vkey -type=tcp -password=secrettest -local_type=secret
+```
+如需指定本地端口可加参数`-local_port=xx`,默认为2000
+
+**注意:** password为web管理上添加的唯一密钥,具体命令可查看web管理上的命令提示
+
+假设10.1.50.2用户名为root,现在执行`ssh -p 2000 root@1.1.1.1`即可访问ssh
+
+
+## p2p服务
+
+**适用范围:**  大流量传输场景,流量不经过公网服务器,但是由于p2p穿透和nat类型关系较大,不保证100%成功,支持大部分nat类型。[nat类型检测](/npc_extend?id=nat类型检测)
+
+**假设场景:**
+
+想通过访问使用端机器(访问端,也就是本机)的2000端口---->访问到内网机器 10.2.50.2的22端口
+
+**使用步骤**
+- 在`nps.conf`中设置`p2p_ip`(nps服务器ip)和`p2p_port`(nps服务器udp端口)
+- 在刚才刚才创建的客户端中添加一条p2p代理,并设置唯一密钥p2pssh
+- 在使用端机器(本机)执行命令
+
+```
+./npc -server=1.1.1.1:8284 -vkey=123 -password=p2pssh -target=10.2.50.2:22
+```
+如需指定本地端口可加参数`-local_port=xx`,默认为2000
+
+**注意:** password为web管理上添加的唯一密钥,具体命令可查看web管理上的命令提示
+
+假设内网机器为10.2.50.2的ssh用户名为root,现在在本机上执行`ssh -p 2000 root@127.0.0.1`即可访问机器2的ssh,如果是网站在浏览器访问127.0.0.1:2000端口即可。

+ 247 - 0
docs/feature.md

@@ -0,0 +1,247 @@
+# 扩展功能
+## 缓存支持
+对于web站点来说,一些静态文件往往消耗更大的流量,且在内网穿透中,静态文件还需到客户端获取一次,这将导致更大的流量消耗。nps在域名解析代理中支持对静态文件进行缓存。
+
+即假设一个站点有a.css,nps将只需从npc客户端读取一次该文件,然后把该文件的内容放在内存中,下一次将不再对npc客户端进行请求而直接返回内存中的对应内容。该功能默认是关闭的,如需开启请在`nps.conf`中设置`http_cache=true`,并设置`http_cache_length`(缓存文件的个数,消耗内存,不宜过大,0表示不限制个数)
+
+## 数据压缩支持
+
+由于是内网穿透,内网客户端与服务端之间的隧道存在大量的数据交换,为节省流量,加快传输速度,由此本程序支持SNNAPY形式的压缩。
+
+
+- 所有模式均支持数据压缩
+- 在web管理或客户端配置文件中设置
+
+
+## 加密传输
+
+如果公司内网防火墙对外网访问进行了流量识别与屏蔽,例如禁止了ssh协议等,通过设置 配置文件,将服务端与客户端之间的通信内容加密传输,将会有效防止流量被拦截。
+- nps使用tls加密,所以一定要保留conf目录下的密钥文件,同时也可以自行生成
+- 在web管理或客户端配置文件中设置
+
+
+
+## 站点保护
+域名代理模式所有客户端共用一个http服务端口,在知道域名后任何人都可访问,一些开发或者测试环境需要保密,所以可以设置用户名和密码,nps将通过 Http Basic Auth 来保护,访问时需要输入正确的用户名和密码。
+
+
+- 在web管理或客户端配置文件中设置
+
+## host修改
+
+由于内网站点需要的host可能与公网域名不一致,域名代理支持host修改功能,即修改request的header中的host字段。
+
+**使用方法:在web管理中设置**
+
+## 自定义header
+
+支持对header进行新增或者修改,以配合服务的需要
+
+## 404页面配置
+支持域名解析模式的自定义404页面,修改/web/static/page/error.html中内容即可,暂不支持静态文件等内容
+
+## 流量限制
+
+支持客户端级流量限制,当该客户端入口流量与出口流量达到设定的总量后会拒绝服务
+,域名代理会返回404页面,其他代理会拒绝连接,使用该功能需要在`nps.conf`中设置`allow_flow_limit`,默认是关闭的。
+
+## 带宽限制
+
+支持客户端级带宽限制,带宽计算方式为入口和出口总和,权重均衡,使用该功能需要在`nps.conf`中设置`allow_rate_limit`,默认是关闭的。
+
+## 负载均衡
+本代理支持域名解析模式和tcp代理的负载均衡,在web域名添加或者编辑中内网目标分行填写多个目标即可实现轮训级别的负载均衡
+
+## 端口白名单
+为了防止服务端上的端口被滥用,可在nps.conf中配置allow_ports限制可开启的端口,忽略或者不填表示端口不受限制,格式:
+
+```ini
+allow_ports=9001-9009,10001,11000-12000
+```
+
+## 端口范围映射
+当客户端以配置文件的方式启动时,可以将本地的端口进行范围映射,仅支持tcp和udp模式,例如:
+
+```ini
+[tcp]
+mode=tcp
+server_port=9001-9009,10001,11000-12000
+target_port=8001-8009,10002,13000-14000
+```
+
+逗号分隔,可单个或者范围,注意上下端口的对应关系,无法一一对应将不能成功
+## 端口范围映射到其他机器
+```ini
+[tcp]
+mode=tcp
+server_port=9001-9009,10001,11000-12000
+target_port=8001-8009,10002,13000-14000
+target_ip=10.1.50.2
+```
+填写target_ip后则表示映射的该地址机器的端口,忽略则便是映射本地127.0.0.1,仅范围映射时有效
+## 守护进程
+本代理支持守护进程,使用示例如下,服务端客户端所有模式通用,支持linux,darwin,windows。
+```
+./(nps|npc) start|stop|restart|status 若有其他参数可加其他参数
+```
+```
+(nps|npc).exe start|stop|restart|status 若有其他参数可加其他参数
+```
+## KCP协议支持
+
+KCP 是一个快速可靠协议,能以比 TCP浪费10%-20%的带宽的代价,换取平均延迟降低 30%-40%,在弱网环境下对性能能有一定的提升。可在nps.conf中修改`bridge_type`为kcp
+,设置后本代理将开启udp端口(`bridge_port`)
+
+注意:当服务端为kcp时,客户端连接时也需要使用相同配置,无配置文件模式加上参数type=kcp,配置文件模式在配置文件中设置tp=kcp
+
+## 域名泛解析
+支持域名泛解析,例如将host设置为*.proxy.com,a.proxy.com、b.proxy.com等都将解析到同一目标,在web管理中或客户端配置文件中将host设置为此格式即可。
+
+## URL路由
+本代理支持根据URL将同一域名转发到不同的内网服务器,可在web中或客户端配置文件中设置,此参数也可忽略,例如在客户端配置文件中
+
+```ini
+[web1]
+host=a.proxy.com
+target_addr=127.0.0.1:7001
+location=/test
+[web2]
+host=a.proxy.com
+target_addr=127.0.0.1:7002
+location=/static
+```
+对于`a.proxy.com/test`将转发到`web1`,对于`a.proxy.com/static`将转发到`web2`
+
+## 限制ip访问
+如果将一些危险性高的端口例如ssh端口暴露在公网上,可能会带来一些风险,本代理支持限制ip访问。
+
+**使用方法:** 在配置文件nps.conf中设置`ip_limit`=true,设置后仅通过注册的ip方可访问。
+
+**ip注册**:
+
+**方式一:**
+在需要访问的机器上,运行客户端
+
+```
+./npc register -server=ip:port -vkey=公钥或客户端密钥 time=2
+```
+
+time为有效小时数,例如time=2,在当前时间后的两小时内,本机公网ip都可以访问nps代理.
+
+**方式二:**
+此外nps的web登陆也可提供验证的功能,成功登陆nps web admin后将自动为登陆的ip注册两小时的允许访问权限。
+
+
+**注意:** 本机公网ip并不是一成不变的,请自行注意有效期的设置,同时同一网络下,多人也可能是在公用同一个公网ip。
+## 客户端最大连接数
+为防止恶意大量长连接,影响服务端程序的稳定性,可以在web或客户端配置文件中为每个客户端设置最大连接数。该功能针对`socks5`、`http正向代理`、`域名代理`、`tcp代理`、`udp代理`、`私密代理`生效,使用该功能需要在`nps.conf`中设置`allow_connection_num_limit=true`,默认是关闭的。
+
+## 客户端最大隧道数限制
+nps支持对客户端的隧道数量进行限制,该功能默认是关闭的,如需开启,请在`nps.conf`中设置`allow_tunnel_num_limit=true`。
+## 端口复用
+在一些严格的网络环境中,对端口的个数等限制较大,nps支持强大端口复用功能。将`bridge_port`、 `http_proxy_port`、 `https_proxy_port` 、`web_port`都设置为同一端口,也能正常使用。
+
+- 使用时将需要复用的端口设置为与`bridge_port`一致即可,将自动识别。
+- 如需将web管理的端口也复用,需要配置`web_host`也就是一个二级域名以便区分
+
+## 多路复用
+
+nps主要通信默认基于多路复用,无需开启。
+
+多路复用基于TCP滑动窗口原理设计,动态计算延迟以及带宽来算出应该往网络管道中打入的流量。
+由于主要通信大多采用TCP协议,并无法探测其实时丢包情况,对于产生丢包重传的情况,采用较大的宽容度,
+5分钟的等待时间,超时将会关闭当前隧道连接并重新建立,这将会抛弃当前所有的连接。
+在Linux上,可以通过调节内核参数来适应不同应用场景。
+
+对于需求大带宽又有一定的丢包的场景,可以保持默认参数不变,尽可能少抛弃连接
+高并发下可根据[Linux系统限制](## Linux系统限制) 调整
+
+对于延迟敏感而又有一定丢包的场景,可以适当调整TCP重传次数
+`tcp_syn_retries`, `tcp_retries1`, `tcp_retries2`
+高并发同上
+nps会在系统主动关闭连接的时候拿到报错,进而重新建立隧道连接
+
+## 环境变量渲染
+npc支持环境变量渲染以适应在某些特殊场景下的要求。
+
+**在无配置文件启动模式下:**
+设置环境变量
+```
+export NPC_SERVER_ADDR=1.1.1.1:8284
+export NPC_SERVER_VKEY=xxxxx
+```
+直接执行./npc即可运行
+
+**在配置文件启动模式下:**
+```ini
+[common]
+server_addr={{.NPC_SERVER_ADDR}}
+conn_type=tcp
+vkey={{.NPC_SERVER_VKEY}}
+auto_reconnection=true
+[web]
+host={{.NPC_WEB_HOST}}
+target_addr={{.NPC_WEB_TARGET}}
+```
+在配置文件中填入相应的环境变量名称,npc将自动进行渲染配置文件替换环境变量
+
+## 健康检查
+
+当客户端以配置文件模式启动时,支持多节点的健康检查。配置示例如下
+
+```ini
+[health_check_test1]
+health_check_timeout=1
+health_check_max_failed=3
+health_check_interval=1
+health_http_url=/
+health_check_type=http
+health_check_target=127.0.0.1:8083,127.0.0.1:8082
+
+[health_check_test2]
+health_check_timeout=1
+health_check_max_failed=3
+health_check_interval=1
+health_check_type=tcp
+health_check_target=127.0.0.1:8083,127.0.0.1:8082
+```
+**health关键词必须在开头存在**
+
+第一种是http模式,也就是以get的方式请求目标+url,返回状态码为200表示成功
+
+第一种是tcp模式,也就是以tcp的方式与目标建立连接,能成功建立连接表示成功
+
+如果失败次数超过`health_check_max_failed`,nps则会移除该npc下的所有该目标,如果失败后目标重新上线,nps将自动将目标重新加入。
+
+项 | 含义
+---|---
+health_check_timeout |  健康检查超时时间
+health_check_max_failed |  健康检查允许失败次数
+health_check_interval |  健康检查间隔
+health_check_type |  健康检查类型
+health_check_target |  健康检查目标,多个以逗号(,)分隔
+health_check_type |  健康检查类型
+health_http_url |  健康检查url,仅http模式适用
+
+## 日志输出
+
+日志输出级别
+
+**对于npc:**
+```
+-log_level=0~7 -log_path=npc.log
+```
+```
+LevelEmergency->0  LevelAlert->1
+
+LevelCritical->2 LevelError->3
+
+LevelWarning->4 LevelNotice->5
+
+LevelInformational->6 LevelDebug->7
+```
+默认为全输出,级别为0到7
+
+**对于nps:**
+
+在`nps.conf`中设置相关配置即可

+ 42 - 0
docs/index.html

@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Document</title>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
+    <meta name="description" content="Description">
+    <meta name="viewport"
+          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
+    <link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css">
+</head>
+<body>
+<div id="app"></div>
+<script src="//unpkg.com/docsify-edit-on-github/index.js"></script>
+
+<script>
+
+    window.$docsify = {
+        name: '',
+        repo: '',
+        loadSidebar: true,
+        coverpage: true,
+        subMaxLevel: 2,
+        maxLevel: 4,
+        search: {
+            noData: "没有结果",
+            paths: 'auto',
+            placeholder: "搜索",
+            hideOtherSidebarContent: true, // whether or not to hide other sidebar content
+        },
+        plugins: [
+            EditOnGithubPlugin.create("https://github.com/ehang-io/nps/tree/master/docs/", "", "在github上编辑"),
+        ]
+
+    }
+</script>
+<script src="//unpkg.com/docsify/lib/docsify.min.js"></script>
+<script src="//unpkg.com/docsify/lib/plugins/search.min.js"></script>
+<script src="//unpkg.com/docsify-copy-code"></script>
+
+</body>
+</html>

+ 18 - 0
docs/install.md

@@ -0,0 +1,18 @@
+# 安装
+## 安装包安装
+ [releases](https://github.com/ehang-io/nps/releases)
+
+下载对应的系统版本即可,服务端和客户端是单独的
+
+## 源码安装
+- 安装源码
+```go get -u ehang.io/nps...```
+- 编译
+
+服务端```go build cmd/nps/nps.go```
+
+客户端```go build cmd/npc/npc.go```
+
+## docker安装
+> [server](https://hub.docker.com/r/ffdfgdfg/nps)
+> [client](https://hub.docker.com/r/ffdfgdfg/npc)

+ 4 - 0
docs/introduction.md

@@ -0,0 +1,4 @@
+![image](https://github.com/ehang-io/nps/blob/master/image/web2.png?raw=true)
+# 介绍
+
+可在网页上配置和管理各个tcp、udp隧道、内网站点代理,http、https解析等,功能强大,操作方便。

BIN
docs/logo.png


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
docs/logo.svg


+ 36 - 0
docs/npc_extend.md

@@ -0,0 +1,36 @@
+# 增强功能
+## nat类型检测
+```
+ ./npc nat
+```
+如果p2p双方都是Symmetric Nat,肯定不能成功,其他组合都有较大成功率。
+## 状态检查
+```
+ ./npc status -config=npc配置文件路径
+```
+## 重载配置文件
+```
+ ./npc restart -config=npc配置文件路径
+```
+
+## 通过代理连接nps
+有时候运行npc的内网机器无法直接访问外网,此时可以可以通过socks5代理连接nps
+
+对于配置文件方式启动,设置
+```ini
+[common]
+proxy_url=socks5://111:222@127.0.0.1:8024
+```
+对于无配置文件模式,加上参数
+
+```
+-proxy=socks5://111:222@127.0.0.1:8024
+```
+支持socks5和http两种模式
+
+即socks5://username:password@ip:port
+
+或http://username:password@ip:port
+
+## 群晖支持
+可在releases中下载spk群晖套件,例如`npc_x64-6.1_0.19.0-1.spk`

+ 23 - 0
docs/npc_sdk.md

@@ -0,0 +1,23 @@
+# npc sdk文档
+
+```
+命令行模式启动客户端
+p0->连接地址
+p1->vkey
+p2->连接类型(tcp or udp)
+p3->连接代理
+
+extern GoInt StartClientByVerifyKey(char* p0, char* p1, char* p2, char* p3);
+
+查看当前启动的客户端状态,在线为1,离线为0
+extern GoInt GetClientStatus();
+
+关闭客户端
+extern void CloseClient();
+
+获取当前客户端版本
+extern char* Version();
+
+获取日志,实时更新
+extern char* Logs();
+```

+ 107 - 0
docs/nps_extend.md

@@ -0,0 +1,107 @@
+# 增强功能
+## 使用https
+
+**方式一:** 类似于nginx实现https的处理
+
+在配置文件中将https_proxy_port设置为443或者其他你想配置的端口,和在web中对应域名编辑中设置对应的证书路径,将`https_just_proxy`设置为false,然后就和http代理一样了
+
+**此外:** 可以在`nps.conf`中设置一个默认的https配置,当遇到未在web中设置https证书的域名解析时,将自动使用默认证书,另还有一种情况就是对于某些请求的clienthello不携带sni扩展信息,nps也将自动使用默认证书
+
+
+**方式二:** 在内网对应服务器上设置https
+
+在`nps.conf`中将`https_just_proxy`设置为true,并且打开`https_proxy_port`端口,然后nps将直接转发https请求到内网服务器上,由内网服务器进行https处理
+
+## 与nginx配合
+
+有时候我们还需要在云服务器上运行nginx来保证静态文件缓存等,本代理可和nginx配合使用,在配置文件中将httpProxyPort设置为非80端口,并在nginx中配置代理,例如httpProxyPort为8024时
+```
+server {
+    listen 80;
+    server_name *.proxy.com;
+    location / {
+        proxy_set_header Host  $http_host;
+        proxy_pass http://127.0.0.1:8024;
+    }
+}
+```
+如需使用https也可在nginx监听443端口并配置ssl,并将本代理的httpsProxyPort设置为空关闭https即可,例如httpProxyPort为8024时
+
+```
+server {
+    listen 443;
+    server_name *.proxy.com;
+    ssl on;
+    ssl_certificate  certificate.crt;
+    ssl_certificate_key private.key;
+    ssl_session_timeout 5m;
+    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
+    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+    ssl_prefer_server_ciphers on;
+    location / {
+        proxy_set_header Host  $http_host;
+        proxy_pass http://127.0.0.1:8024;
+    }
+}
+```
+## web管理使用https
+如果web管理需要使用https,可以在配置文件`nps.conf`中设置`web_open_ssl=true`,并配置`web_cert_file`和`web_key_file`
+## web使用Caddy代理
+
+如果将web配置到Caddy代理,实现子路径访问nps,可以这样配置.
+
+假设我们想通过 `http://caddy_ip:caddy_port/nps` 来访问后台, Caddyfile 这样配置:
+
+```Caddyfile
+caddy_ip:caddy_port/nps {
+  ##server_ip 为 nps 服务器IP
+  ##web_port 为 nps 后台端口
+  proxy / http://server_ip:web_port/nps {
+	transparent
+  }
+}
+```
+
+nps.conf 修改 `web_base_url` 为 `/nps` 即可
+```
+web_base_url=/nps
+```
+
+
+## 关闭代理
+
+如需关闭http代理可在配置文件中将http_proxy_port设置为空,如需关闭https代理可在配置文件中将https_proxy_port设置为空。
+
+## 流量数据持久化
+服务端支持将流量数据持久化,默认情况下是关闭的,如果有需求可以设置`nps.conf`中的`flow_store_interval`参数,单位为分钟
+
+**注意:** nps不会持久化通过公钥连接的客户端
+## 系统信息显示
+nps服务端支持在web上显示和统计服务器的相关信息,但默认一些统计图表是关闭的,如需开启请在`nps.conf`中设置`system_info_display=true`
+
+## 自定义客户端连接密钥
+web上可以自定义客户端连接的密钥,但是必须具有唯一性
+## 关闭公钥访问
+可以将`nps.conf`中的`public_vkey`设置为空或者删除
+
+## 关闭web管理
+可以将`nps.conf`中的`web_port`设置为空或者删除
+
+## 服务端多用户登陆
+如果将`nps.conf`中的`allow_user_login`设置为true,服务端web将支持多用户登陆,登陆用户名为user,默认密码为每个客户端的验证密钥,登陆后可以进入客户端编辑修改web登陆的用户名和密码,默认该功能是关闭的。
+
+## 用户注册功能
+nps服务端支持用户注册功能,可将`nps.conf`中的`allow_user_register`设置为true,开启后登陆页将会有有注册功能,
+
+## 监听指定ip
+
+nps支持每个隧道监听不同的服务端端口,在`nps.conf`中设置`allow_multi_ip=true`后,可在web中控制,或者npc配置文件中(可忽略,默认为0.0.0.0)
+```ini
+server_ip=xxx
+```
+## 代理到服务端本地
+在使用nps监听80或者443端口时,默认是将所有的请求都会转发到内网上,但有时候我们的nps服务器的上一些服务也需要使用这两个端口,nps提供类似于`nginx` `proxy_pass` 的功能,支持将代理到服务器本地,该功能支持域名解析,tcp、udp隧道,默认关闭。
+
+**即:** 假设在nps的vps服务器上有一个服务使用5000端口,这时候nps占用了80端口和443,我们想能使用一个域名通过http(s)访问到5000的服务。
+
+**使用方式:** 在`nps.conf`中设置`allow_local_proxy=true`,然后在web上设置想转发的隧道或者域名然后选择转发到本地选项即可成功。

+ 45 - 0
docs/nps_use.md

@@ -0,0 +1,45 @@
+# 使用
+**提示:使用web模式时,服务端执行文件必须在项目根目录,否则无法正确加载配置文件**
+
+## web管理
+
+进入web界面,公网ip:web界面端口(默认8080),密码默认为123
+
+进入web管理界面,有详细的说明
+
+## 服务端配置文件重载
+对于linux、darwin
+```shell
+ sudo nps reload
+```
+对于windows
+```shell
+ nps.exe reload
+```
+**说明:** 仅支持部分配置重载,例如`allow_user_login` `auth_crypt_key` `auth_key` `web_username` `web_password` 等,未来将支持更多
+
+
+## 服务端停止或重启
+对于linux、darwin
+```shell
+ sudo nps stop|restart
+```
+对于windows
+```shell
+ nps.exe stop|restart
+```
+## 服务端更新
+请首先执行`sudo nps stop`或者`nps.exe stop`停止运行,然后
+
+对于linux
+```shell
+ sudo nps-update update
+```
+对于windows
+```shell
+ nps-update.exe update
+```
+
+更新完成后,执行执行`sudo nps start`或者`nps.exe start`重新运行即可完成升级
+
+如果无法更新成功,可以直接自行下载releases压缩包然后覆盖原有的nps二进制文件和web目录

+ 35 - 0
docs/run.md

@@ -0,0 +1,35 @@
+# 启动
+## 服务端
+下载完服务器压缩包后,解压,然后进入解压后的文件夹
+
+- 执行安装命令
+
+对于linux|darwin ```sudo ./nps install```
+
+对于windows,管理员身份运行cmd,进入安装目录 ```nps.exe install```
+
+- 启动
+
+对于linux|darwin ```sudo nps start```
+
+对于windows,管理员身份运行cmd,进入程序目录 ```nps.exe start```
+
+```安装后windows配置文件位于 C:\Program Files\nps,linux和darwin位于/etc/nps```
+
+停止和重启可用,stop和restart
+
+**如果发现没有启动成功,可以使用`nps(.exe) stop`,然后运行`nps.(exe)`运行调试,或查看日志**(Windows日志文件位于当前运行目录下,linux和darwin位于/var/log/nps.log)
+- 访问服务端ip:web服务端口(默认为8080)
+- 使用用户名和密码登陆(默认admin/123,正式使用一定要更改)
+- 创建客户端
+
+## 客户端
+- 下载客户端安装包并解压,进入到解压目录
+- 点击web管理中客户端前的+号,复制启动命令
+- 执行启动命令,linux直接执行即可,windows将./npc换成npc.exe用cmd执行
+
+如果需要注册到系统服务可查看[注册到系统服务](/use?id=注册到系统服务)
+
+## 配置
+- 客户端连接后,在web中配置对应穿透服务即可
+- 可以查看[使用示例](/example)

+ 21 - 0
docs/server_config.md

@@ -0,0 +1,21 @@
+# 服务端配置文件
+- /conf/nps.conf
+
+名称 | 含义
+---|---
+web_port | web管理端口
+web_password | web界面管理密码
+web_username | web界面管理账号
+web_base_url | web管理主路径,用于将web管理置于代理子路径后面
+bridge_port  | 服务端客户端通信端口
+https_proxy_port | 域名代理https代理监听端口
+http_proxy_port | 域名代理http代理监听端口
+auth_key|web api密钥
+bridge_type|客户端与服务端连接方式kcp或tcp
+public_vkey|客户端以配置文件模式启动时的密钥,设置为空表示关闭客户端配置文件连接模式
+ip_limit|是否限制ip访问,true或false或忽略
+flow_store_interval|服务端流量数据持久化间隔,单位分钟,忽略表示不持久化
+log_level|日志输出级别
+auth_crypt_key | 获取服务端authKey时的aes加密密钥,16位
+p2p_ip| 服务端Ip,使用p2p模式必填
+p2p_port|p2p模式开启的udp端口

+ 5 - 0
docs/thanks.md

@@ -0,0 +1,5 @@
+Thanks [jetbrains](https://www.jetbrains.com/?from=nps) for providing development tools for nps
+
+<html>
+<img src="https://ftp.bmp.ovh/imgs/2019/12/6435398b0c7402b1.png" width="300"  align=center />
+</html>

+ 221 - 0
docs/use.md

@@ -0,0 +1,221 @@
+# 基本使用
+## 无配置文件模式
+此模式的各种配置在服务端web管理中完成,客户端除运行一条命令外无需任何其他设置
+```
+ ./npc -server=ip:port -vkey=web界面中显示的密钥
+```
+## 注册到系统服务(开机启动、守护进程)
+对于linux、darwin
+- 注册:`sudo ./npc install 其他参数(例如-server=xx -vkey=xx或者-config=xxx)`
+- 启动:`sudo npc start`
+- 停止:`sudo npc stop`
+- 如果需要更换命令内容需要先卸载`./npc uninstall`,再重新注册
+
+对于windows,使用管理员身份运行cmd
+
+- 注册:`npc.exe install 其他参数(例如-server=xx -vkey=xx或者-config=xxx)`
+- 启动:`npc.exe start`
+- 停止:`npc.exe stop`
+- 如果需要更换命令内容需要先卸载`npc.exe uninstall`,再重新注册
+
+注册到服务后,日志文件windows位于当前目录下,linux和darwin位于/var/log/npc.log
+
+## 客户端更新
+首先进入到对于的客户端二进制文件目录
+
+请首先执行`sudo npc stop`或者`npc.exe stop`停止运行,然后
+
+对于linux
+```shell
+ sudo npc-update update
+```
+对于windows
+```shell
+npc-update.exe update
+```
+
+更新完成后,执行执行`sudo npc start`或者`npc.exe start`重新运行即可完成升级
+
+如果无法更新成功,可以直接自行下载releases压缩包然后覆盖原有的npc二进制文件
+
+## 配置文件模式
+此模式使用nps的公钥或者客户端私钥验证,各种配置在客户端完成,同时服务端web也可以进行管理
+```
+ ./npc -config=npc配置文件路径
+```
+## 配置文件说明
+[示例配置文件](https://github.com/ehang-io/nps/tree/master/conf/npc.conf)
+#### 全局配置
+```ini
+[common]
+server_addr=1.1.1.1:8284
+conn_type=tcp
+vkey=123
+username=111
+password=222
+compress=true
+crypt=true
+rate_limit=10000
+flow_limit=100
+remark=test
+max_conn=10
+```
+项 | 含义
+---|---
+server_addr | 服务端ip:port
+conn_type | 与服务端通信模式(tcp或kcp)
+vkey|服务端配置文件中的密钥(非web)
+username|socks5或http(s)密码保护用户名(可忽略)
+password|socks5或http(s)密码保护密码(可忽略)
+compress|是否压缩传输(true或false或忽略)
+crypt|是否加密传输(true或false或忽略)
+rate_limit|速度限制,可忽略
+flow_limit|流量限制,可忽略
+remark|客户端备注,可忽略
+max_conn|最大连接数,可忽略
+#### 域名代理
+
+```ini
+[common]
+server_addr=1.1.1.1:8284
+vkey=123
+[web1]
+host=a.proxy.com
+target_addr=127.0.0.1:8080,127.0.0.1:8082
+host_change=www.proxy.com
+header_set_proxy=nps
+```
+项 | 含义
+---|---
+web1 | 备注
+host | 域名(http|https都可解析)
+target_addr|内网目标,负载均衡时多个目标,逗号隔开
+host_change|请求host修改
+header_xxx|请求header修改或添加,header_proxy表示添加header proxy:nps
+
+#### tcp隧道模式
+
+```ini
+[common]
+server_addr=1.1.1.1:8284
+vkey=123
+[tcp]
+mode=tcp
+target_addr=127.0.0.1:8080
+server_port=9001
+```
+项 | 含义
+---|---
+mode | tcp
+server_port | 在服务端的代理端口
+tartget_addr|内网目标
+
+#### udp隧道模式
+
+```ini
+[common]
+server_addr=1.1.1.1:8284
+vkey=123
+[udp]
+mode=udp
+target_addr=127.0.0.1:8080
+server_port=9002
+```
+项 | 含义
+---|---
+mode | udp
+server_port | 在服务端的代理端口
+target_addr|内网目标
+#### http代理模式
+
+```ini
+[common]
+server_addr=1.1.1.1:8284
+vkey=123
+[http]
+mode=httpProxy
+server_port=9003
+```
+项 | 含义
+---|---
+mode | httpProxy
+server_port | 在服务端的代理端口
+#### socks5代理模式
+
+```ini
+[common]
+server_addr=1.1.1.1:8284
+vkey=123
+[socks5]
+mode=socks5
+server_port=9004
+multi_account=multi_account.conf
+```
+项 | 含义
+---|---
+mode | socks5
+server_port | 在服务端的代理端口
+multi_account | socks5多账号配置文件(可选),配置后使用basic_username和basic_password无法通过认证
+#### 私密代理模式
+
+```ini
+[common]
+server_addr=1.1.1.1:8284
+vkey=123
+[secret_ssh]
+mode=secret
+password=ssh2
+target_addr=10.1.50.2:22
+```
+项 | 含义
+---|---
+mode | secret
+password | 唯一密钥
+target_addr|内网目标
+
+#### p2p代理模式
+
+```ini
+[common]
+server_addr=1.1.1.1:8284
+vkey=123
+[p2p_ssh]
+mode=p2p
+password=ssh2
+target_addr=10.1.50.2:22
+```
+项 | 含义
+---|---
+mode | p2p
+password | 唯一密钥
+target_addr|内网目标
+
+
+#### 文件访问模式
+利用nps提供一个公网可访问的本地文件服务,此模式仅客户端使用配置文件模式方可启动
+
+```ini
+[common]
+server_addr=1.1.1.1:8284
+vkey=123
+[file]
+mode=file
+server_port=9100
+local_path=/tmp/
+strip_pre=/web/
+````
+
+项 | 含义
+---|---
+mode | file
+server_port | 服务端开启的端口
+local_path|本地文件目录
+strip_pre|前缀
+
+对于`strip_pre`,访问公网`ip:9100/web/`相当于访问`/tmp/`目录
+
+#### 断线重连
+```ini
+[common]
+auto_reconnection=true
+```

+ 233 - 0
docs/webapi.md

@@ -0,0 +1,233 @@
+获取客户端列表
+
+```
+POST /client/list/
+```
+
+
+| 参数 | 含义 |
+| --- | --- |
+| search | 搜索 |
+| order | 排序asc 正序 desc倒序 |
+| offset | 分页(第几页) |
+| limit | 条数(分页显示的条数) |
+
+***
+获取单个客户端
+
+```
+POST /client/getclient/
+```
+
+
+| 参数 | 含义 |
+| --- | --- |
+| id | 客户端id |
+
+***
+添加客户端
+
+```
+POST /client/add/
+```
+
+| 参数 | 含义 |
+| --- | --- |
+| remark | 备注 |
+| u | basic权限认证用户名 |
+| p | basic权限认证密码 |
+| limit | 条数(分页显示的条数) |
+| vkey | 客户端验证密钥 |
+| config\_conn\_allow | 是否允许客户端以配置文件模式连接 1允许 0不允许 |
+| compress | 压缩1允许 0不允许 |
+| crypt | 是否加密(1或者0)1允许 0不允许 |
+| rate\_limit | 带宽限制 单位KB/S 空则为不限制 |
+| flow\_limit | 流量限制 单位M 空则为不限制 |
+| max\_conn | 客户端最大连接数量 空则为不限制 |
+| max\_tunnel | 客户端最大隧道数量 空则为不限制 |
+
+***
+修改客户端(25.4版本有问题暂时不能用)
+
+```
+POST /client/edit/
+```
+
+| 参数 | 含义 |
+| --- | --- |
+| remark | 备注 |
+| u | basic权限认证用户名 |
+| p | basic权限认证密码 |
+| limit | 条数(分页显示的条数) |
+| vkey | 客户端验证密钥 |
+| config\_conn\_allow | 是否允许客户端以配置文件模式连接 1允许 0不允许 |
+| compress | 压缩1允许 0不允许 |
+| crypt | 是否加密(1或者0)1允许 0不允许 |
+| rate\_limit | 带宽限制 单位KB/S 空则为不限制 |
+| flow\_limit | 流量限制 单位M 空则为不限制 |
+| max\_conn | 客户端最大连接数量 空则为不限制 |
+| max\_tunnel | 客户端最大隧道数量 空则为不限制 |
+| id | 要修改的客户端id |
+
+***
+删除客户端
+
+```
+POST /client/del/
+```
+
+| 参数 | 含义 |
+| --- | --- |
+| id | 要删除的客户端id |
+
+***
+获取域名解析列表
+
+```
+POST /index/hostlist/
+```
+
+| 参数 | 含义 |
+| --- | --- |
+| search | 搜索(可以搜域名/备注什么的) |
+| offset | 分页(第几页) |
+| limit | 条数(分页显示的条数) |
+
+***
+添加域名解析
+
+```
+POST /index/addhost/
+```
+
+
+| 参数 | 含义 |
+| --- | --- |
+| remark | 备注 |
+| host | 域名 |
+| scheme | 协议类型(三种 all http https) |
+| location | url路由 空则为不限制 |
+| client\_id | 客户端id |
+| target | 内网目标(ip:端口) |
+| header | request header 请求头 |
+| hostchange | request host 请求主机 |
+
+***
+修改域名解析
+
+```
+POST /index/edithost/
+```
+
+| 参数 | 含义 |
+| --- | --- |
+| remark | 备注 |
+| host | 域名 |
+| scheme | 协议类型(三种 all http https) |
+| location | url路由 空则为不限制 |
+| client\_id | 客户端id |
+| target | 内网目标(ip:端口) |
+| header | request header 请求头 |
+| hostchange | request host 请求主机 |
+| id | 需要修改的域名解析id |
+
+***
+删除域名解析
+
+```
+POST /index/delhost/
+```
+
+| 参数 | 含义 |
+| --- | --- |
+| id | 需要删除的域名解析id |
+
+***
+获取单条隧道信息
+
+```
+POST /index/getonetunnel/
+```
+
+| 参数 | 含义 |
+| --- | --- |
+| id | 隧道的id |
+
+***
+获取隧道列表
+
+```
+POST /index/gettunnel/
+```
+
+| 参数 | 含义 |
+| --- | --- |
+| client\_id | 穿透隧道的客户端id |
+| type | 类型tcp udp httpProx socks5 secret p2p |
+| search | 搜索 |
+| offset | 分页(第几页) |
+| limit | 条数(分页显示的条数) |
+
+***
+添加隧道
+
+```
+POST /index/add/
+```
+
+| 参数 | 含义 |
+| --- | --- |
+| type | 类型tcp udp httpProx socks5 secret p2p |
+| remark | 备注 |
+| port | 服务端端口 |
+| target | 目标(ip:端口) |
+| client\_id | 客户端id |
+
+***
+修改隧道
+
+```
+POST /index/edit/
+```
+
+| 参数 | 含义 |
+| --- | --- |
+| type | 类型tcp udp httpProx socks5 secret p2p |
+| remark | 备注 |
+| port | 服务端端口 |
+| target | 目标(ip:端口) |
+| client\_id | 客户端id |
+| id | 隧道id |
+
+***
+删除隧道
+
+```
+POST /index/del/
+```
+
+| 参数 | 含义 |
+| --- | --- |
+| id | 隧道id |
+
+***
+隧道停止工作
+
+```
+POST /index/stop/
+```
+
+| 参数 | 含义 |
+| --- | --- |
+| id | 隧道id |
+
+***
+隧道开始工作
+
+```
+POST /index/start/
+```
+
+| 参数 | 含义 |
+| --- | --- |
+| id | 隧道id |

+ 12 - 8
go.mod

@@ -1,28 +1,32 @@
-module github.com/cnlh/nps
+module ehang.io/nps
 
-go 1.12
+go 1.13
 
 require (
+	fyne.io/fyne v1.2.0
 	github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
 	github.com/astaxie/beego v1.12.0
-	github.com/belogik/goes v0.0.0-20151229125003-e54d722c3aff // indirect
+	github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c // indirect
+	github.com/c4milo/unpackit v0.0.0-20170704181138-4ed373e9ef1c
 	github.com/ccding/go-stun v0.0.0-20180726100737-be486d185f3d
+	github.com/dsnet/compress v0.0.1 // indirect
 	github.com/go-ole/go-ole v1.2.4 // indirect
 	github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db
+	github.com/hooklift/assert v0.0.0-20170704181755-9d1defd6d214 // indirect
+	github.com/kardianos/service v1.0.0
 	github.com/klauspost/cpuid v1.2.1 // indirect
+	github.com/klauspost/pgzip v1.2.1 // indirect
 	github.com/klauspost/reedsolomon v1.9.2 // indirect
-	github.com/onsi/gomega v1.5.0 // indirect
 	github.com/panjf2000/ants/v2 v2.2.2
-	github.com/pkg/errors v0.8.0
+	github.com/pkg/errors v0.8.1
 	github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 // indirect
-	github.com/shirou/gopsutil v2.18.12+incompatible
-	github.com/stretchr/testify v1.3.0 // indirect
+	github.com/shirou/gopsutil v2.19.11+incompatible
 	github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect
 	github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b // indirect
 	github.com/tjfoc/gmsm v1.0.1 // indirect
 	github.com/xtaci/kcp-go v5.4.4+incompatible
 	github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae // indirect
-	golang.org/x/net v0.0.0-20181114220301-adae6a3d119a
+	golang.org/x/net v0.0.0-20181220203305-927f97764cc3
 	golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa // indirect
 )
 

+ 59 - 19
go.sum

@@ -1,13 +1,18 @@
+fyne.io/fyne v1.2.0 h1:mdp7Cs7QmSJTeazYxEDa9wWeJNig7paBcjm0dooFtLE=
+fyne.io/fyne v1.2.0/go.mod h1:Ab+3DIB/FVteW0y4DXfmZv4N3JdnCBh2lHkINI02BOU=
 github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
+github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
 github.com/OwnLocal/goes v1.0.0/go.mod h1:8rIFjBGTue3lCU0wplczcUgt9Gxgrkkrw7etMIcn8TM=
 github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
 github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
-github.com/astaxie/beego v1.12.0 h1:MRhVoeeye5N+Flul5PoVfD9CslfdoH+xqC/xvSQ5u2Y=
-github.com/astaxie/beego v1.12.0/go.mod h1:fysx+LZNZKnvh4GED/xND7jWtjCR6HzydR2Hh2Im57o=
+github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
 github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ=
 github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU=
-github.com/belogik/goes v0.0.0-20151229125003-e54d722c3aff/go.mod h1:PhH1ZhyCzHKt4uAasyx+ljRCgoezetRNf59CUtwUkqY=
 github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
+github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c h1:FUUopH4brHNO2kJoNN3pV+OBEYmgraLT/KHZrMM69r0=
+github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
+github.com/c4milo/unpackit v0.0.0-20170704181138-4ed373e9ef1c h1:aprLqMn7gSPT+vdDSl+/E6NLEuArwD/J7IWd8bJt5lQ=
+github.com/c4milo/unpackit v0.0.0-20170704181138-4ed373e9ef1c/go.mod h1:Ie6SubJv/NTO9Q0UBH0QCl3Ve50lu9hjbi5YJUw03TE=
 github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwtzR1KE=
 github.com/ccding/go-stun v0.0.0-20180726100737-be486d185f3d h1:As4937T5NVbJ/DmZT9z33pyLEprMd6CUSfhbmMY57Io=
 github.com/ccding/go-stun v0.0.0-20180726100737-be486d185f3d/go.mod h1:3FK1bMar37f7jqVY7q/63k3OMX1c47pGCufzt3X0sYE=
@@ -18,50 +23,75 @@ github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFl
 github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY=
 github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
+github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
+github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
 github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
 github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk=
 github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
 github.com/exfly/beego v1.12.0-export-init h1:VQNYKdXhAwZGUaFmQv8Aj921O3rQJZRIF8xeGrhsjrI=
 github.com/exfly/beego v1.12.0-export-init/go.mod h1:fysx+LZNZKnvh4GED/xND7jWtjCR6HzydR2Hh2Im57o=
-github.com/exfly/beego v1.12.0 h1:OXwIwngaAx35Mga+jLiZmArusBxj8/H0jYXzGDAdwOg=
-github.com/exfly/beego v1.12.0/go.mod h1:fysx+LZNZKnvh4GED/xND7jWtjCR6HzydR2Hh2Im57o=
+github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 h1:SCYMcCJ89LjRGwEa0tRluNRiMjZHalQZrVrvTbPh+qw=
+github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=
+github.com/go-gl/glfw v0.0.0-20181213070059-819e8ce5125f h1:7MsFMbSn8Lcw0blK4+NEOf8DuHoOBDhJsHz04yh13pM=
+github.com/go-gl/glfw v0.0.0-20181213070059-819e8ce5125f/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
 github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
 github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
 github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8=
+github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/hooklift/assert v0.0.0-20170704181755-9d1defd6d214 h1:WgfvpuKg42WVLkxNwzfFraXkTXPK36bMqXvMFN67clI=
+github.com/hooklift/assert v0.0.0-20170704181755-9d1defd6d214/go.mod h1:kj6hFWqfwSjFjLnYW5PK1DoxZ4O0uapwHRmd9jhln4E=
+github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc=
+github.com/josephspurrier/goversioninfo v0.0.0-20190124120936-8611f5a5ff3f/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE=
+github.com/kardianos/service v1.0.0 h1:HgQS3mFfOlyntWX8Oke98JcJLqt1DBcHR4kxShpYef0=
+github.com/kardianos/service v1.0.0/go.mod h1:8CzDhVuCuugtsHyZoTvsOBuvonN/UDBvl0kH+BUxvbo=
+github.com/klauspost/compress v1.4.1 h1:8VMb5+0wMgdBykOV96DwNwKFQ+WTI4pzYURP99CcB9E=
+github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w=
 github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
+github.com/klauspost/pgzip v1.2.1 h1:oIPZROsWuPHpOdMVWLuJZXwgjhrW8r1yEX8UqMyeNHM=
+github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
 github.com/klauspost/reedsolomon v1.9.2 h1:E9CMS2Pqbv+C7tsrYad4YC9MfhnMVWhMRsTi7U0UB18=
 github.com/klauspost/reedsolomon v1.9.2/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4=
 github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
 github.com/panjf2000/ants/v2 v2.2.2 h1:TWzusBjq/IflXhy+/S6u5wmMLCBdJnB9tPIx9Zmhvok=
 github.com/panjf2000/ants/v2 v2.2.2/go.mod h1:1GFm8bV8nyCQvU5K4WvBCTG1/YBFOD2VzjffD8fV55A=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 h1:X+yvsM2yrEktyI+b2qND5gpH8YhURn0k8OCaeRnkINo=
 github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=
-github.com/shirou/gopsutil v2.18.12+incompatible h1:1eaJvGomDnH74/5cF4CTmTbLHAriGFsTZppLXDX93OM=
-github.com/shirou/gopsutil v2.18.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
+github.com/shirou/gopsutil v2.19.11+incompatible h1:lJHR0foqAjI4exXqWsU3DbH7bX1xvdhGdnXTIARA9W4=
+github.com/shirou/gopsutil v2.19.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
 github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw=
 github.com/siddontang/ledisdb v0.0.0-20181029004158-becf5f38d373/go.mod h1:mF1DpOSOUiJRMR+FDqaqu3EBqrybQtrDDszLUZ6oxPg=
 github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/srwiley/oksvg v0.0.0-20190829233741-58e08c8fe40e h1:LJUrNHytcMXWKxnULIHPe5SCb1jDpO9o672VB1x2EuQ=
+github.com/srwiley/oksvg v0.0.0-20190829233741-58e08c8fe40e/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4=
+github.com/srwiley/rasterx v0.0.0-20181219215540-696f7edb7a7e h1:FFotfUvew9Eg02LYRl8YybAnm0HCwjjfY5JlOI1oB00=
+github.com/srwiley/rasterx v0.0.0-20181219215540-696f7edb7a7e/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU=
 github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709 h1:Ko2LQMrRU+Oy/+EDBwX7eZ2jp3C47eDBB8EIhKTun+I=
+github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
 github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 h1:89CEmDvlq/F7SJEOqkIdNDGJXrQIhuIx9D2DBXjavSU=
 github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU=
@@ -69,6 +99,8 @@ github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b h1:mnG1fcsIB1d/3vbkB
 github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4=
 github.com/tjfoc/gmsm v1.0.1 h1:R11HlqhXkDospckjZEihx9SW/2VW0RgdwrykyWMFOQU=
 github.com/tjfoc/gmsm v1.0.1/go.mod h1:XxO4hdhhrzAd+G4CjDqaOkd0hUzmtPR/d3EiBBMn/wc=
+github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
+github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
 github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc=
 github.com/xtaci/kcp-go v5.4.4+incompatible h1:QIJ0a0Q0N1G20yLHL2+fpdzyy2v/Cb3PI+xiwx/KK9c=
 github.com/xtaci/kcp-go v5.4.4+incompatible/go.mod h1:bN6vIwHQbfHaHtFpEssmWsN45a+AZwO7eyRCmEIbtvE=
@@ -76,17 +108,25 @@ github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+A
 github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE=
 golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85 h1:et7+NAX3lLIk5qUCTA9QelBjGE/NkhzYw/mhnr0s7nI=
 golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa h1:KIDDMLT1O0Nr7TSxp8xM5tJcdn8tgyAONntO829og1M=
 golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

+ 37 - 0
gui/npc/AndroidManifest.xml

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:versionCode="1"
+        android:versionName="0.26.0"
+        package="org.nps.client"
+        platformBuildVersionCode="15"
+        platformBuildVersionName="4.0.4-1406430">
+
+    <uses-permission
+            android:name="android.permission.INTERNET"/>
+
+    <application
+            android:label="Npc"
+            android:debuggable="true">
+
+        <activity
+                android:label="Npc"
+                android:name="org.golang.app.GoNativeActivity"
+                android:configChanges="0xa0">
+
+            <meta-data
+                    android:name="android.app.lib_name"
+                    android:value="npc"/>
+
+            <intent-filter>
+
+                <action
+                        android:name="android.intent.action.MAIN"/>
+
+                <category
+                        android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
+

+ 173 - 0
gui/npc/npc.go

@@ -0,0 +1,173 @@
+package main
+
+import (
+	"ehang.io/nps/client"
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/daemon"
+	"ehang.io/nps/lib/version"
+	"fmt"
+	"fyne.io/fyne"
+	"fyne.io/fyne/app"
+	"fyne.io/fyne/layout"
+	"fyne.io/fyne/widget"
+	"github.com/astaxie/beego/logs"
+	"io/ioutil"
+	"os"
+	"path"
+	"runtime"
+	"strings"
+	"time"
+)
+
+func main() {
+	daemon.InitDaemon("npc", common.GetRunPath(), common.GetTmpPath())
+	logs.SetLogger("store")
+	application := app.New()
+	window := application.NewWindow("Npc " + version.VERSION)
+	window.SetContent(WidgetScreen())
+	window.Resize(fyne.NewSize(910, 350))
+
+	window.ShowAndRun()
+
+}
+
+var (
+	start    bool
+	status   = "Start!"
+	connType = "tcp"
+	cl       = new(client.TRPClient)
+)
+
+func WidgetScreen() fyne.CanvasObject {
+	return fyne.NewContainerWithLayout(layout.NewBorderLayout(nil, nil, nil, nil),
+		makeMainTab(),
+	)
+}
+
+func makeMainTab() fyne.Widget {
+	serverPort := widget.NewEntry()
+	serverPort.SetPlaceHolder("Server:Port")
+
+	vKey := widget.NewEntry()
+	vKey.SetPlaceHolder("Vkey")
+
+	radio := widget.NewRadio([]string{"tcp", "kcp"}, func(s string) { connType = s })
+	radio.Horizontal = true
+
+	refreshCh := make(chan struct{})
+	button := widget.NewButton(status, func() {
+		start = !start
+		if start {
+			status = "Stop!"
+			// init the npc
+			fmt.Println("submit", serverPort.Text, vKey.Text, connType)
+			sp, vk, ct := loadConfig()
+			if sp != serverPort.Text || vk != vKey.Text || ct != connType {
+				saveConfig(serverPort.Text, vKey.Text, connType)
+			}
+			cl = client.NewRPClient(serverPort.Text, vKey.Text, connType, "", nil)
+			go cl.Start()
+		} else {
+			// close the npc
+			status = "Start!"
+			if cl != nil {
+				go cl.Close()
+				cl = nil
+			}
+		}
+		refreshCh <- struct{}{}
+	})
+	go func() {
+		for {
+			<-refreshCh
+			button.SetText(status)
+		}
+	}()
+
+	lo := widget.NewMultiLineEntry()
+	lo.SetReadOnly(true)
+	lo.Resize(fyne.NewSize(910, 250))
+	slo := widget.NewScrollContainer(lo)
+	slo.Resize(fyne.NewSize(910, 250))
+	go func() {
+		for {
+			time.Sleep(time.Second)
+			lo.SetText(common.GetLogMsg())
+			slo.Resize(fyne.NewSize(910, 250))
+		}
+	}()
+
+	sp, vk, ct := loadConfig()
+	if sp != "" && vk != "" && ct != "" {
+		serverPort.SetText(sp)
+		vKey.SetText(vk)
+		connType = ct
+		radio.SetSelected(ct)
+	}
+
+	return widget.NewVBox(
+		widget.NewLabel("Npc "+version.VERSION),
+		serverPort,
+		vKey,
+		radio,
+		button,
+		slo,
+	)
+}
+
+func getDir() (dir string, err error) {
+	if runtime.GOOS != "android" {
+		dir, err = os.UserConfigDir()
+		if err != nil {
+			return
+		}
+	} else {
+		dir = "/data/data/org.nps.client/files"
+	}
+	return
+}
+
+func saveConfig(host, vkey, connType string) {
+	data := strings.Join([]string{host, vkey, connType}, "\n")
+	ph, err := getDir()
+	if err != nil {
+		logs.Warn("not found config dir")
+		return
+	}
+	_ = os.Remove(path.Join(ph, "npc.conf"))
+	f, err := os.OpenFile(path.Join(ph, "npc.conf"), os.O_CREATE|os.O_WRONLY, 0644)
+	defer f.Close()
+	if err != nil {
+		logs.Error(err)
+		return
+	}
+	if _, err := f.Write([]byte(data)); err != nil {
+		f.Close() // ignore error; Write error takes precedence
+		logs.Error(err)
+		return
+	}
+}
+
+func loadConfig() (host, vkey, connType string) {
+	ph, err := getDir()
+	if err != nil {
+		logs.Warn("not found config dir")
+		return
+	}
+	f, err := os.OpenFile(path.Join(ph, "npc.conf"), os.O_RDONLY, 0644)
+	defer f.Close()
+	if err != nil {
+		logs.Error(err)
+		return
+	}
+	data, err := ioutil.ReadAll(f)
+	if err != nil {
+		logs.Error(err)
+		return
+	}
+	li := strings.Split(string(data), "\n")
+	host = li[0]
+	vkey = li[1]
+	connType = li[2]
+	return
+}

+ 821 - 0
image/work_flow.svg

@@ -0,0 +1,821 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- 由 Microsoft Visio, SVG Export 生成 工作图.svg Page-1 -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events"
+		xmlns:v="http://schemas.microsoft.com/visio/2003/SVGExtensions/" width="11.6929in" height="8.26772in"
+		viewBox="0 0 841.89 595.276" xml:space="preserve" color-interpolation-filters="sRGB" class="st17">
+	<v:documentProperties v:langID="2052" v:metric="true" v:viewMarkup="false">
+		<v:userDefs>
+			<v:ud v:nameU="msvNoAutoConnect" v:prompt="" v:val="VT0(0):26"/>
+		</v:userDefs>
+	</v:documentProperties>
+
+	<style type="text/css">
+	<![CDATA[
+		.st1 {fill:#5b9bd5;stroke:#ffffff;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.5}
+		.st2 {stroke:#ffffff;stroke-linecap:butt;stroke-width:0.5}
+		.st3 {fill:#1e4a73;font-family:Calibri;font-size:1.5em}
+		.st4 {fill:#aad288;stroke:#ffffff;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.5}
+		.st5 {fill:#ffffff}
+		.st6 {font-size:1em}
+		.st7 {fill:#5b9bd5}
+		.st8 {stroke:#ffffff;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.5}
+		.st9 {fill:#a5a5a5}
+		.st10 {marker-end:url(#mrkr4-135);marker-start:url(#mrkr4-133);stroke:#5592c9;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5}
+		.st11 {fill:#5592c9;fill-opacity:1;stroke:#5592c9;stroke-opacity:1;stroke-width:0.37313432835821}
+		.st12 {fill:#ffffff;stroke:none;stroke-linecap:butt;stroke-width:7.2}
+		.st13 {fill:#41729d;font-family:Calibri;font-size:1.5em}
+		.st14 {fill:none;stroke:#42829e;stroke-dasharray:45,27;stroke-linecap:round;stroke-linejoin:round;stroke-width:3}
+		.st15 {fill:none;stroke:none;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.5}
+		.st16 {fill:#5b9bd5;font-family:Calibri;font-size:1.99999em}
+		.st17 {fill:none;fill-rule:evenodd;font-size:12px;overflow:visible;stroke-linecap:square;stroke-miterlimit:3}
+	]]>
+	</style>
+
+	<defs id="Markers">
+		<g id="lend4">
+			<path d="M 2 1 L 0 0 L 2 -1 L 2 1 " style="stroke:none"/>
+		</g>
+		<marker id="mrkr4-133" class="st11" v:arrowType="4" v:arrowSize="2" v:setback="5.12" refX="5.12" orient="auto"
+				markerUnits="strokeWidth" overflow="visible">
+			<use xlink:href="#lend4" transform="scale(2.68) "/>
+		</marker>
+		<marker id="mrkr4-135" class="st11" v:arrowType="4" v:arrowSize="2" v:setback="5.36" refX="-5.36" orient="auto"
+				markerUnits="strokeWidth" overflow="visible">
+			<use xlink:href="#lend4" transform="scale(-2.68,-2.68) "/>
+		</marker>
+	</defs>
+	<g v:mID="0" v:index="1" v:groupContext="foregroundPage">
+		<v:userDefs>
+			<v:ud v:nameU="msvThemeOrder" v:val="VT0(0):26"/>
+		</v:userDefs>
+		<title>页-1</title>
+		<v:pageProperties v:drawingScale="0.0393701" v:pageScale="0.0393701" v:drawingUnits="24" v:shadowOffsetX="8.50394"
+				v:shadowOffsetY="-8.50394"/>
+		<v:layer v:name="连接线" v:index="0"/>
+		<g id="group1-1" transform="translate(418.11,-311.811)" v:mID="1" v:groupContext="group">
+			<v:custProps>
+				<v:cp v:nameU="AssetNumber" v:lbl="资产号" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="SerialNumber" v:lbl="序列号" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Location" v:lbl="位置" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Building" v:lbl="构建" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Room" v:lbl="空间" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Manufacturer" v:lbl="制造商" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="ProductNumber" v:lbl="产品编号" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="PartNumber" v:lbl="部件号" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="ProductDescription" v:lbl="产品说明" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="ShapeClass" v:lbl="ShapeClass" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true"
+						v:ask="false" v:langID="2052" v:cal="0" v:val="VT4(设备)"/>
+				<v:cp v:nameU="ShapeType" v:lbl="ShapeType" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true"
+						v:ask="false" v:langID="2052" v:cal="0" v:val="VT4(设备)"/>
+				<v:cp v:nameU="SubShapeType" v:lbl="SubShapeType" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true"
+						v:ask="false" v:langID="2052" v:cal="0" v:val="VT4(防火墙)"/>
+			</v:custProps>
+			<v:userDefs>
+				<v:ud v:nameU="visVersion" v:prompt="" v:val="VT0(15):26"/>
+				<v:ud v:nameU="ShapeClass" v:prompt="" v:val="VT0(5):26"/>
+				<v:ud v:nameU="SolSH" v:prompt="" v:val="VT15({BF0433D9-CD73-4EB5-8390-8653BE590246}):41"/>
+				<v:ud v:nameU="visLegendShape" v:prompt="" v:val="VT0(2):26"/>
+			</v:userDefs>
+			<title>防火墙</title>
+			<desc>NAT</desc>
+			<g id="shape2-2" v:mID="2" v:groupContext="shape" transform="translate(0.545123,-7.63784)">
+				<title>工作表.2</title>
+				<rect x="0" y="539.685" width="69.7759" height="55.5905" class="st1"/>
+			</g>
+			<g id="shape3-4" v:mID="3" v:groupContext="shape" transform="translate(0.795123,-7.88784)">
+				<title>工作表.3</title>
+				<v:userDefs>
+					<v:ud v:nameU="SurroundingRegionColor" v:prompt="" v:val="VT5(1)"/>
+					<v:ud v:nameU="SurroundingRegionColor" v:prompt="" v:val="VT5(#5b9bd5)"/>
+				</v:userDefs>
+				<path d="M0 587.41 L69.28 587.41" class="st2"/>
+				<path d="M0 579.54 L69.28 579.54" class="st2"/>
+				<path d="M0 571.67 L69.28 571.67" class="st2"/>
+				<path d="M0 563.8 L69.28 563.8" class="st2"/>
+				<path d="M0 555.93 L69.28 555.93" class="st2"/>
+				<path d="M0 548.06 L69.28 548.06" class="st2"/>
+				<path d="M17.32 595.28 L17.32 587.41" class="st2"/>
+				<path d="M34.64 595.28 L34.64 587.41" class="st2"/>
+				<path d="M51.96 595.28 L51.96 587.41" class="st2"/>
+				<path d="M8.66 587.41 L8.66 579.54" class="st2"/>
+				<path d="M25.98 587.41 L25.98 579.54" class="st2"/>
+				<path d="M43.3 587.41 L43.3 579.54" class="st2"/>
+				<path d="M60.62 587.41 L60.62 579.54" class="st2"/>
+				<path d="M17.32 579.54 L17.32 571.67" class="st2"/>
+				<path d="M34.64 579.54 L34.64 571.67" class="st2"/>
+				<path d="M51.96 579.54 L51.96 571.67" class="st2"/>
+				<path d="M8.66 571.67 L8.66 563.8" class="st2"/>
+				<path d="M25.98 571.67 L25.98 563.8" class="st2"/>
+				<path d="M43.3 571.67 L43.3 563.8" class="st2"/>
+				<path d="M60.62 571.67 L60.62 563.8" class="st2"/>
+				<path d="M17.32 563.8 L17.32 555.93" class="st2"/>
+				<path d="M34.64 563.8 L34.64 555.93" class="st2"/>
+				<path d="M51.96 563.8 L51.96 555.93" class="st2"/>
+				<path d="M8.66 555.93 L8.66 548.06" class="st2"/>
+				<path d="M25.98 555.93 L25.98 548.06" class="st2"/>
+				<path d="M43.3 555.93 L43.3 548.06" class="st2"/>
+				<path d="M60.62 555.93 L60.62 548.06" class="st2"/>
+				<path d="M17.32 548.06 L17.32 540.19" class="st2"/>
+				<path d="M34.64 548.06 L34.64 540.19" class="st2"/>
+				<path d="M51.96 548.06 L51.96 540.19" class="st2"/>
+			</g>
+			<g id="shape1-36" v:mID="1" v:groupContext="groupContent">
+				<v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
+				<v:textRect cx="35.4331" cy="610.077" width="42.88" height="29.6036"/>
+				<text x="20.03" y="615.48" class="st3" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>NAT</text>			</g>
+		</g>
+		<g id="group4-38" transform="translate(701.575,-311.811)" v:mID="4" v:groupContext="group">
+			<v:custProps>
+				<v:cp v:nameU="AssetNumber" v:lbl="资产号" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="SerialNumber" v:lbl="序列号" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Location" v:lbl="位置" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Building" v:lbl="构建" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Room" v:lbl="空间" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Manufacturer" v:lbl="制造商" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="ProductNumber" v:lbl="产品编号" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="PartNumber" v:lbl="部件号" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="ProductDescription" v:lbl="产品说明" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="NetworkName" v:lbl="网络名称" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="IPAddress" v:lbl="IP 地址" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="SubnetMask" v:lbl="子网掩码" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="AdminInterface" v:lbl="管理接口" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="NumberofPorts" v:lbl="端口数目" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="CommunityString" v:lbl="团体字符串" v:prompt="" v:type="0" v:format="" v:sortKey="Network"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="NetworkDescription" v:lbl="网络说明" v:prompt="" v:type="0" v:format="" v:sortKey="Network"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="MACAddress" v:lbl="MAC 地址" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="CPU" v:lbl="CPU" v:prompt="" v:type="0" v:format="" v:sortKey="Workstation" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Memory" v:lbl="内存" v:prompt="" v:type="0" v:format="" v:sortKey="Workstation" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="OperatingSystem" v:lbl="操作系统" v:prompt="" v:type="0" v:format="" v:sortKey="Workstation"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="HardDriveSize" v:lbl="硬盘容量" v:prompt="" v:type="0" v:format="" v:sortKey="Workstation"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Department" v:lbl="部门" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="ShapeClass" v:lbl="ShapeClass" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true"
+						v:ask="false" v:langID="2052" v:cal="0" v:val="VT4(设备)"/>
+				<v:cp v:nameU="ShapeType" v:lbl="ShapeType" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true"
+						v:ask="false" v:langID="2052" v:cal="0" v:val="VT4(服务器)"/>
+				<v:cp v:nameU="BelongsTo" v:lbl="属于" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+			</v:custProps>
+			<v:userDefs>
+				<v:ud v:nameU="visVersion" v:prompt="" v:val="VT0(15):26"/>
+				<v:ud v:nameU="ShapeClass" v:prompt="" v:val="VT0(5):26"/>
+				<v:ud v:nameU="SolSH" v:prompt="" v:val="VT15({BF0433D9-CD73-4EB5-8390-8653BE590246}):41"/>
+				<v:ud v:nameU="visLegendShape" v:prompt="" v:val="VT0(2):26"/>
+			</v:userDefs>
+			<title>服务器</title>
+			<desc>Application2 10.0.0.4:PORT</desc>
+			<g id="shape5-39" v:mID="5" v:groupContext="shape" transform="translate(12.8133,0)">
+				<title>工作表.5</title>
+				<rect x="0" y="524.409" width="45.2395" height="70.8661" class="st1"/>
+			</g>
+			<g id="shape6-41" v:mID="6" v:groupContext="shape" transform="translate(46.625,-30.2513)">
+				<title>工作表.6</title>
+				<ellipse cx="2.73472" cy="592.541" rx="2.73472" ry="2.73472" class="st4"/>
+			</g>
+			<g id="shape7-43" v:mID="7" v:groupContext="shape" transform="translate(30.0295,-11.6164)">
+				<title>工作表.7</title>
+				<v:userDefs>
+					<v:ud v:nameU="SurroundingRegionColor" v:prompt="" v:val="VT5(1)"/>
+					<v:ud v:nameU="SurroundingRegionColor" v:prompt="" v:val="VT5(#5b9bd5)"/>
+				</v:userDefs>
+				<path d="M-0 595.28 L22.06 595.28 L22.06 593.5 L-0 593.5 L-0 595.28 ZM-0 589.9 L22.06 589.9 L22.06 588.13 L-0 588.13
+							 L-0 589.9 ZM-0 584.53 L22.06 584.53 L22.06 582.76 L-0 582.76 L-0 584.53 Z" class="st5"/>
+			</g>
+			<g id="shape4-46" v:mID="4" v:groupContext="groupContent">
+				<v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
+				<v:textRect cx="35.4331" cy="620.877" width="115.9" height="51.2036"/>
+				<text x="-10.6" y="615.48" class="st3" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>Application2<v:newlineChar/><tspan
+							x="-16.48" dy="1.2em" class="st6">10.0.0.4:PORT</tspan></text>			</g>
+		</g>
+		<g id="group8-49" transform="translate(701.575,-496.063)" v:mID="8" v:groupContext="group">
+			<v:custProps>
+				<v:cp v:nameU="AssetNumber" v:lbl="资产号" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="SerialNumber" v:lbl="序列号" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Location" v:lbl="位置" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Building" v:lbl="构建" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Room" v:lbl="空间" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Manufacturer" v:lbl="制造商" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="ProductNumber" v:lbl="产品编号" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="PartNumber" v:lbl="部件号" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="ProductDescription" v:lbl="产品说明" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="NetworkName" v:lbl="网络名称" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="IPAddress" v:lbl="IP 地址" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="SubnetMask" v:lbl="子网掩码" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="AdminInterface" v:lbl="管理接口" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="NumberofPorts" v:lbl="端口数目" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="CommunityString" v:lbl="团体字符串" v:prompt="" v:type="0" v:format="" v:sortKey="Network"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="NetworkDescription" v:lbl="网络说明" v:prompt="" v:type="0" v:format="" v:sortKey="Network"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="MACAddress" v:lbl="MAC 地址" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="CPU" v:lbl="CPU" v:prompt="" v:type="0" v:format="" v:sortKey="Workstation" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Memory" v:lbl="内存" v:prompt="" v:type="0" v:format="" v:sortKey="Workstation" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="OperatingSystem" v:lbl="操作系统" v:prompt="" v:type="0" v:format="" v:sortKey="Workstation"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="HardDriveSize" v:lbl="硬盘容量" v:prompt="" v:type="0" v:format="" v:sortKey="Workstation"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Department" v:lbl="部门" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="ShapeClass" v:lbl="ShapeClass" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true"
+						v:ask="false" v:langID="2052" v:cal="0" v:val="VT4(设备)"/>
+				<v:cp v:nameU="ShapeType" v:lbl="ShapeType" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true"
+						v:ask="false" v:langID="2052" v:cal="0" v:val="VT4(服务器)"/>
+				<v:cp v:nameU="BelongsTo" v:lbl="属于" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+			</v:custProps>
+			<v:userDefs>
+				<v:ud v:nameU="visVersion" v:prompt="" v:val="VT0(15):26"/>
+				<v:ud v:nameU="ShapeClass" v:prompt="" v:val="VT0(5):26"/>
+				<v:ud v:nameU="SolSH" v:prompt="" v:val="VT15({BF0433D9-CD73-4EB5-8390-8653BE590246}):41"/>
+				<v:ud v:nameU="visLegendShape" v:prompt="" v:val="VT0(2):26"/>
+			</v:userDefs>
+			<title>服务器.8</title>
+			<desc>Application1 10.0.0.3:PORT</desc>
+			<g id="shape9-50" v:mID="9" v:groupContext="shape" transform="translate(12.8133,0)">
+				<title>工作表.9</title>
+				<rect x="0" y="524.409" width="45.2395" height="70.8661" class="st1"/>
+			</g>
+			<g id="shape10-52" v:mID="10" v:groupContext="shape" transform="translate(46.625,-30.2513)">
+				<title>工作表.10</title>
+				<ellipse cx="2.73472" cy="592.541" rx="2.73472" ry="2.73472" class="st4"/>
+			</g>
+			<g id="shape11-54" v:mID="11" v:groupContext="shape" transform="translate(30.0295,-11.6164)">
+				<title>工作表.11</title>
+				<v:userDefs>
+					<v:ud v:nameU="SurroundingRegionColor" v:prompt="" v:val="VT5(1)"/>
+					<v:ud v:nameU="SurroundingRegionColor" v:prompt="" v:val="VT5(#5b9bd5)"/>
+				</v:userDefs>
+				<path d="M-0 595.28 L22.06 595.28 L22.06 593.5 L-0 593.5 L-0 595.28 ZM-0 589.9 L22.06 589.9 L22.06 588.13 L-0 588.13
+							 L-0 589.9 ZM-0 584.53 L22.06 584.53 L22.06 582.76 L-0 582.76 L-0 584.53 Z" class="st5"/>
+			</g>
+			<g id="shape8-57" v:mID="8" v:groupContext="groupContent">
+				<v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
+				<v:textRect cx="35.4331" cy="620.877" width="115.9" height="51.2036"/>
+				<text x="-10.6" y="615.48" class="st3" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>Application1<v:newlineChar/><tspan
+							x="-16.48" dy="1.2em" class="st6">10.0.0.3:PORT</tspan></text>			</g>
+		</g>
+		<g id="group12-60" transform="translate(701.575,-127.559)" v:mID="12" v:groupContext="group">
+			<v:custProps>
+				<v:cp v:nameU="AssetNumber" v:lbl="资产号" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="SerialNumber" v:lbl="序列号" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Location" v:lbl="位置" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Building" v:lbl="构建" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Room" v:lbl="空间" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Manufacturer" v:lbl="制造商" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="ProductNumber" v:lbl="产品编号" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="PartNumber" v:lbl="部件号" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="ProductDescription" v:lbl="产品说明" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="NetworkName" v:lbl="网络名称" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="IPAddress" v:lbl="IP 地址" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="SubnetMask" v:lbl="子网掩码" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="AdminInterface" v:lbl="管理接口" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="NumberofPorts" v:lbl="端口数目" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="CommunityString" v:lbl="团体字符串" v:prompt="" v:type="0" v:format="" v:sortKey="Network"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="NetworkDescription" v:lbl="网络说明" v:prompt="" v:type="0" v:format="" v:sortKey="Network"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="MACAddress" v:lbl="MAC 地址" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="CPU" v:lbl="CPU" v:prompt="" v:type="0" v:format="" v:sortKey="Workstation" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Memory" v:lbl="内存" v:prompt="" v:type="0" v:format="" v:sortKey="Workstation" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="OperatingSystem" v:lbl="操作系统" v:prompt="" v:type="0" v:format="" v:sortKey="Workstation"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="HardDriveSize" v:lbl="硬盘容量" v:prompt="" v:type="0" v:format="" v:sortKey="Workstation"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Department" v:lbl="部门" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="ShapeClass" v:lbl="ShapeClass" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true"
+						v:ask="false" v:langID="2052" v:cal="0" v:val="VT4(设备)"/>
+				<v:cp v:nameU="ShapeType" v:lbl="ShapeType" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true"
+						v:ask="false" v:langID="2052" v:cal="0" v:val="VT4(服务器)"/>
+				<v:cp v:nameU="BelongsTo" v:lbl="属于" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+			</v:custProps>
+			<v:userDefs>
+				<v:ud v:nameU="visVersion" v:prompt="" v:val="VT0(15):26"/>
+				<v:ud v:nameU="ShapeClass" v:prompt="" v:val="VT0(5):26"/>
+				<v:ud v:nameU="SolSH" v:prompt="" v:val="VT15({BF0433D9-CD73-4EB5-8390-8653BE590246}):41"/>
+				<v:ud v:nameU="visLegendShape" v:prompt="" v:val="VT0(2):26"/>
+			</v:userDefs>
+			<title>服务器.12</title>
+			<desc>Application3 10.0.0.5:PORT</desc>
+			<g id="shape13-61" v:mID="13" v:groupContext="shape" transform="translate(12.8133,0)">
+				<title>工作表.13</title>
+				<rect x="0" y="524.409" width="45.2395" height="70.8661" class="st1"/>
+			</g>
+			<g id="shape14-63" v:mID="14" v:groupContext="shape" transform="translate(46.625,-30.2513)">
+				<title>工作表.14</title>
+				<ellipse cx="2.73472" cy="592.541" rx="2.73472" ry="2.73472" class="st4"/>
+			</g>
+			<g id="shape15-65" v:mID="15" v:groupContext="shape" transform="translate(30.0295,-11.6164)">
+				<title>工作表.15</title>
+				<v:userDefs>
+					<v:ud v:nameU="SurroundingRegionColor" v:prompt="" v:val="VT5(1)"/>
+					<v:ud v:nameU="SurroundingRegionColor" v:prompt="" v:val="VT5(#5b9bd5)"/>
+				</v:userDefs>
+				<path d="M-0 595.28 L22.06 595.28 L22.06 593.5 L-0 593.5 L-0 595.28 ZM-0 589.9 L22.06 589.9 L22.06 588.13 L-0 588.13
+							 L-0 589.9 ZM-0 584.53 L22.06 584.53 L22.06 582.76 L-0 582.76 L-0 584.53 Z" class="st5"/>
+			</g>
+			<g id="shape12-68" v:mID="12" v:groupContext="groupContent">
+				<v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
+				<v:textRect cx="35.4331" cy="620.877" width="115.9" height="51.2036"/>
+				<text x="-10.6" y="615.48" class="st3" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>Application3<v:newlineChar/><tspan
+							x="-16.48" dy="1.2em" class="st6">10.0.0.5:PORT</tspan></text>			</g>
+		</g>
+		<g id="group16-71" transform="translate(538.583,-311.811)" v:mID="16" v:groupContext="group">
+			<v:custProps>
+				<v:cp v:nameU="AssetNumber" v:lbl="资产号" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="SerialNumber" v:lbl="序列号" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Location" v:lbl="位置" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Building" v:lbl="建筑物" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Room" v:lbl="空间" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Manufacturer" v:lbl="制造商" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="ProductNumber" v:lbl="产品编号" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="PartNumber" v:lbl="部件号" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="ProductDescription" v:lbl="产品说明" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="NetworkName" v:lbl="网络名称" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="IPAddress" v:lbl="IP 地址" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="SubnetMask" v:lbl="子网掩码" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="AdminInterface" v:lbl="管理接口" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="NumberofPorts" v:lbl="端口数目" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="CommunityString" v:lbl="团体字符串" v:prompt="" v:type="0" v:format="" v:sortKey="Network"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="NetworkDescription" v:lbl="网络说明" v:prompt="" v:type="0" v:format="" v:sortKey="Network"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="MACAddress" v:lbl="MAC 地址" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="ShapeClass" v:lbl="ShapeClass" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true"
+						v:ask="false" v:langID="2052" v:cal="0" v:val="VT4(设备)"/>
+				<v:cp v:nameU="ShapeType" v:lbl="ShapeType" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true"
+						v:ask="false" v:langID="2052" v:cal="0" v:val="VT4(计算机)"/>
+				<v:cp v:nameU="SubShapeType" v:lbl="SubShapeType" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true"
+						v:ask="false" v:langID="2052" v:cal="0" v:val="VT4(大型机)"/>
+			</v:custProps>
+			<v:userDefs>
+				<v:ud v:nameU="visVersion" v:prompt="" v:val="VT0(15):26"/>
+				<v:ud v:nameU="ShapeClass" v:prompt="" v:val="VT0(5):26"/>
+				<v:ud v:nameU="SolSH" v:prompt="" v:val="VT15({BF0433D9-CD73-4EB5-8390-8653BE590246}):41"/>
+				<v:ud v:nameU="visLegendShape" v:prompt="" v:val="VT0(2):26"/>
+			</v:userDefs>
+			<title>主机</title>
+			<desc>NPC Client 10.0.0.2 Dial To: -&#62;10.0.0.3:PORT -&#62;10.0.0.4:PORT ...</desc>
+			<g id="shape17-72" v:mID="17" v:groupContext="shape" transform="translate(5.68158,0)">
+				<title>工作表.17</title>
+				<path d="M0 595.28 L59.5 595.28 L59.5 524.41 L0 524.41 L0 595.28 Z" class="st7"/>
+				<path d="M0 595.28 L59.5 595.28 L59.5 524.41 L0 524.41 L0 595.28" class="st8"/>
+				<path d="M29.75 595.28 L29.75 524.41" class="st8"/>
+			</g>
+			<g id="shape18-76" v:mID="18" v:groupContext="shape" transform="translate(11.5726,-5.14536)">
+				<title>工作表.18</title>
+				<v:userDefs>
+					<v:ud v:nameU="SurroundingRegionColor" v:prompt="" v:val="VT5(1)"/>
+					<v:ud v:nameU="SurroundingRegionColor" v:prompt="" v:val="VT5(#5b9bd5)"/>
+				</v:userDefs>
+				<path d="M36.09 551.42 L49.23 551.42 L49.23 536.38 L36.09 536.38 L36.09 551.42 ZM14.89 542.47 L16.68 542.47 L16.68
+							 536.06 L14.89 536.06 L14.89 542.47 ZM14.89 551.71 L16.68 551.71 L16.68 545.3 L14.89 545.3 L14.89 551.71
+							 ZM10.63 542.47 L12.43 542.47 L12.43 536.06 L10.63 536.06 L10.63 542.47 ZM10.63 551.71 L12.43 551.71
+							 L12.43 545.3 L10.63 545.3 L10.63 551.71 ZM4.25 542.47 L6.05 542.47 L6.05 536.06 L4.25 536.06 L4.25 542.47
+							 ZM4.25 551.71 L6.05 551.71 L6.05 545.3 L4.25 545.3 L4.25 551.71 ZM0 542.47 L1.79 542.47 L1.79 536.06
+							 L0 536.06 L0 542.47 ZM0 551.71 L1.79 551.71 L1.79 545.3 L0 545.3 L0 551.71 ZM33.82 586.45 L33.82 587.7
+							 L49.39 587.69 L49.39 586.44 L33.82 586.45 ZM33.82 590.24 L33.82 591.49 L49.39 591.48 L49.39 590.24 L33.82
+							 590.24 ZM33.82 594.03 L33.82 595.28 L49.39 595.27 L49.39 594.03 L33.82 594.03 ZM2.96 587.7 L18.52 587.7
+							 L18.52 586.45 L2.95 586.45 L2.96 587.7 ZM2.96 591.49 L18.52 591.49 L18.52 590.24 L2.95 590.24 L2.96
+							 591.49 ZM2.96 595.28 L18.52 595.28 L18.52 594.03 L2.95 594.03 L2.96 595.28 Z" class="st5"/>
+			</g>
+			<g id="shape19-79" v:mID="19" v:groupContext="shape" transform="translate(49.0815,-50.4059)">
+				<title>工作表.19</title>
+				<path d="M8.15 583.79 A1.11073 1.11073 -180 1 0 10.24 584.56 A1.11073 1.11073 -180 1 0 8.15 583.79 ZM8.17 587.08
+							 A1.11073 1.11073 -180 1 0 10.22 587.93 A1.11073 1.11073 -180 1 0 8.17 587.08 ZM8.18 590.39 A1.11073
+							 1.11073 -180 1 0 10.21 591.28 A1.11073 1.11073 -180 1 0 8.18 590.39 ZM8.18 593.71 A1.11073 1.11073 -180
+							 1 0 10.21 594.6 A1.11073 1.11073 -180 1 0 8.18 593.71 ZM4.1 583.84 A1.11073 1.11073 -180 1 0 6.21 584.51
+							 A1.11073 1.11073 -180 1 0 4.1 583.84 ZM4.11 587.14 A1.11073 1.11073 -180 1 0 6.2 587.87 A1.11073 1.11073
+							 -180 1 0 4.11 587.14 ZM4.11 590.44 A1.11073 1.11073 -180 1 0 6.19 591.22 A1.11073 1.11073 -180 1 0 4.11
+							 590.44 ZM4.11 593.77 A1.11073 1.11073 -180 1 0 6.19 594.55 A1.11073 1.11073 -180 1 0 4.11 593.77 ZM0.04
+							 583.9 A1.11144 1.11144 -180 1 0 2.19 584.46 A1.11144 1.11144 -180 1 0 0.04 583.9 ZM0.05 587.19 A1.11144
+							 1.11144 -180 1 0 2.18 587.82 A1.11144 1.11144 -180 1 0 0.05 587.19 ZM0.05 590.5 A1.11144 1.11144 -180
+							 1 0 2.17 591.16 A1.11144 1.11144 -180 1 0 0.05 590.5 ZM0.05 593.83 A1.11144 1.11144 -180 1 0 2.17 594.49
+							 A1.11144 1.11144 -180 1 0 0.05 593.83 Z" class="st9"/>
+			</g>
+			<g id="shape16-82" v:mID="16" v:groupContext="groupContent">
+				<v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
+				<v:textRect cx="35.4331" cy="674.877" width="130.38" height="159.204"/>
+				<text x="20.18" y="615.48" class="st3" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>NPC<v:newlineChar/><tspan
+							x="14.28" dy="1.2em" class="st6">Client<v:newlineChar/></tspan><tspan x="5.81" dy="1.2em" class="st6">10.0.0.2<v:newlineChar/></tspan><tspan
+							x="7.88" dy="1.2em" class="st6">Dial To:<v:newlineChar/></tspan><tspan x="-23.72" dy="1.2em"
+							class="st6">-</tspan>&#62;10.0.0.3:PORT<v:newlineChar/><tspan x="-23.72" dy="1.2em" class="st6">-</tspan>&#62;10.0.0.4:PORT<v:newlineChar/><tspan
+							x="-23.72" dy="1.2em" class="st6">-</tspan>&#62;10.0.0.5:PORT</text>			</g>
+		</g>
+		<g id="group20-90" transform="translate(212.598,-311.811)" v:mID="20" v:groupContext="group">
+			<v:custProps>
+				<v:cp v:nameU="AssetNumber" v:lbl="资产号" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="SerialNumber" v:lbl="序列号" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Location" v:lbl="位置" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Building" v:lbl="建筑物" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Room" v:lbl="空间" v:prompt="" v:type="0" v:format="" v:sortKey="Asset" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Manufacturer" v:lbl="制造商" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="ProductNumber" v:lbl="产品编号" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="PartNumber" v:lbl="部件号" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="ProductDescription" v:lbl="产品说明" v:prompt="" v:type="0" v:format="" v:sortKey="Equipment"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="NetworkName" v:lbl="网络名称" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="IPAddress" v:lbl="IP 地址" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="SubnetMask" v:lbl="子网掩码" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="AdminInterface" v:lbl="管理接口" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="NumberofPorts" v:lbl="端口数目" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="CommunityString" v:lbl="团体字符串" v:prompt="" v:type="0" v:format="" v:sortKey="Network"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="NetworkDescription" v:lbl="网络说明" v:prompt="" v:type="0" v:format="" v:sortKey="Network"
+						v:invis="false" v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="MACAddress" v:lbl="MAC 地址" v:prompt="" v:type="0" v:format="" v:sortKey="Network" v:invis="false"
+						v:ask="false" v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="ShapeClass" v:lbl="ShapeClass" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true"
+						v:ask="false" v:langID="2052" v:cal="0" v:val="VT4(设备)"/>
+				<v:cp v:nameU="ShapeType" v:lbl="ShapeType" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true"
+						v:ask="false" v:langID="2052" v:cal="0" v:val="VT4(计算机)"/>
+				<v:cp v:nameU="SubShapeType" v:lbl="SubShapeType" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true"
+						v:ask="false" v:langID="2052" v:cal="0" v:val="VT4(大型机)"/>
+			</v:custProps>
+			<v:userDefs>
+				<v:ud v:nameU="visVersion" v:prompt="" v:val="VT0(15):26"/>
+				<v:ud v:nameU="ShapeClass" v:prompt="" v:val="VT0(5):26"/>
+				<v:ud v:nameU="SolSH" v:prompt="" v:val="VT15({BF0433D9-CD73-4EB5-8390-8653BE590246}):41"/>
+				<v:ud v:nameU="visLegendShape" v:prompt="" v:val="VT0(2):26"/>
+			</v:userDefs>
+			<title>主机.20</title>
+			<desc>NPS Server 1.1.1.1 Listen On: 8003-&#62;10.0.0.3:PORT 8004-&#62;10.0....</desc>
+			<g id="shape21-91" v:mID="21" v:groupContext="shape" transform="translate(5.68158,0)">
+				<title>工作表.21</title>
+				<path d="M0 595.28 L59.5 595.28 L59.5 524.41 L0 524.41 L0 595.28 Z" class="st7"/>
+				<path d="M0 595.28 L59.5 595.28 L59.5 524.41 L0 524.41 L0 595.28" class="st8"/>
+				<path d="M29.75 595.28 L29.75 524.41" class="st8"/>
+			</g>
+			<g id="shape22-95" v:mID="22" v:groupContext="shape" transform="translate(11.5726,-5.14536)">
+				<title>工作表.22</title>
+				<v:userDefs>
+					<v:ud v:nameU="SurroundingRegionColor" v:prompt="" v:val="VT5(1)"/>
+					<v:ud v:nameU="SurroundingRegionColor" v:prompt="" v:val="VT5(#5b9bd5)"/>
+				</v:userDefs>
+				<path d="M36.09 551.42 L49.23 551.42 L49.23 536.38 L36.09 536.38 L36.09 551.42 ZM14.89 542.47 L16.68 542.47 L16.68
+							 536.06 L14.89 536.06 L14.89 542.47 ZM14.89 551.71 L16.68 551.71 L16.68 545.3 L14.89 545.3 L14.89 551.71
+							 ZM10.63 542.47 L12.43 542.47 L12.43 536.06 L10.63 536.06 L10.63 542.47 ZM10.63 551.71 L12.43 551.71
+							 L12.43 545.3 L10.63 545.3 L10.63 551.71 ZM4.25 542.47 L6.05 542.47 L6.05 536.06 L4.25 536.06 L4.25 542.47
+							 ZM4.25 551.71 L6.05 551.71 L6.05 545.3 L4.25 545.3 L4.25 551.71 ZM0 542.47 L1.79 542.47 L1.79 536.06
+							 L0 536.06 L0 542.47 ZM0 551.71 L1.79 551.71 L1.79 545.3 L0 545.3 L0 551.71 ZM33.82 586.45 L33.82 587.7
+							 L49.39 587.69 L49.39 586.44 L33.82 586.45 ZM33.82 590.24 L33.82 591.49 L49.39 591.48 L49.39 590.24 L33.82
+							 590.24 ZM33.82 594.03 L33.82 595.28 L49.39 595.27 L49.39 594.03 L33.82 594.03 ZM2.96 587.7 L18.52 587.7
+							 L18.52 586.45 L2.95 586.45 L2.96 587.7 ZM2.96 591.49 L18.52 591.49 L18.52 590.24 L2.95 590.24 L2.96
+							 591.49 ZM2.96 595.28 L18.52 595.28 L18.52 594.03 L2.95 594.03 L2.96 595.28 Z" class="st5"/>
+			</g>
+			<g id="shape23-98" v:mID="23" v:groupContext="shape" transform="translate(49.0815,-50.4059)">
+				<title>工作表.23</title>
+				<path d="M8.15 583.79 A1.11073 1.11073 -180 1 0 10.24 584.56 A1.11073 1.11073 -180 1 0 8.15 583.79 ZM8.17 587.08
+							 A1.11073 1.11073 -180 1 0 10.22 587.93 A1.11073 1.11073 -180 1 0 8.17 587.08 ZM8.18 590.39 A1.11073
+							 1.11073 -180 1 0 10.21 591.28 A1.11073 1.11073 -180 1 0 8.18 590.39 ZM8.18 593.71 A1.11073 1.11073 -180
+							 1 0 10.21 594.6 A1.11073 1.11073 -180 1 0 8.18 593.71 ZM4.1 583.84 A1.11073 1.11073 -180 1 0 6.21 584.51
+							 A1.11073 1.11073 -180 1 0 4.1 583.84 ZM4.11 587.14 A1.11073 1.11073 -180 1 0 6.2 587.87 A1.11073 1.11073
+							 -180 1 0 4.11 587.14 ZM4.11 590.44 A1.11073 1.11073 -180 1 0 6.19 591.22 A1.11073 1.11073 -180 1 0 4.11
+							 590.44 ZM4.11 593.77 A1.11073 1.11073 -180 1 0 6.19 594.55 A1.11073 1.11073 -180 1 0 4.11 593.77 ZM0.04
+							 583.9 A1.11144 1.11144 -180 1 0 2.19 584.46 A1.11144 1.11144 -180 1 0 0.04 583.9 ZM0.05 587.19 A1.11144
+							 1.11144 -180 1 0 2.18 587.82 A1.11144 1.11144 -180 1 0 0.05 587.19 ZM0.05 590.5 A1.11144 1.11144 -180
+							 1 0 2.17 591.16 A1.11144 1.11144 -180 1 0 0.05 590.5 ZM0.05 593.83 A1.11144 1.11144 -180 1 0 2.17 594.49
+							 A1.11144 1.11144 -180 1 0 0.05 593.83 Z" class="st9"/>
+			</g>
+			<g id="shape20-101" v:mID="20" v:groupContext="groupContent">
+				<v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
+				<v:textRect cx="35.4331" cy="674.877" width="166.87" height="159.204"/>
+				<text x="20.84" y="615.48" class="st3" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>NPS<v:newlineChar/><tspan
+							x="12" dy="1.2em" class="st6">Server<v:newlineChar/></tspan><tspan x="10.37" dy="1.2em" class="st6">1.1.1.1<v:newlineChar/></tspan><tspan
+							x="-1.29" dy="1.2em" class="st6">Listen On:<v:newlineChar/></tspan><tspan x="-41.96" dy="1.2em"
+							class="st6">8003</tspan>-&#62;10.0.0.3:PORT<v:newlineChar/><tspan x="-41.96" dy="1.2em" class="st6">8004</tspan>-&#62;10.0.0.4:PORT<v:newlineChar/><tspan
+							x="-41.96" dy="1.2em" class="st6">8005</tspan>-&#62;10.0.0.5:PORT</text>			</g>
+		</g>
+		<g id="group24-109" transform="translate(49.6063,-496.063)" v:mID="24" v:groupContext="group">
+			<v:custProps>
+				<v:cp v:nameU="Name" v:lbl="名称" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Location" v:lbl="位置" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Building" v:lbl="构建" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Room" v:lbl="空间" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Department" v:lbl="部门" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="ShapeClass" v:lbl="ShapeClass" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true"
+						v:ask="false" v:langID="2052" v:cal="0" v:val="VT4(连接性)"/>
+				<v:cp v:nameU="ShapeType" v:lbl="ShapeType" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true"
+						v:ask="false" v:langID="2052" v:cal="0" v:val="VT4(用户)"/>
+			</v:custProps>
+			<v:userDefs>
+				<v:ud v:nameU="visVersion" v:prompt="" v:val="VT0(15):26"/>
+				<v:ud v:nameU="ShapeClass" v:prompt="" v:val="VT0(5):26"/>
+				<v:ud v:nameU="SolSH" v:prompt="" v:val="VT15({BF0433D9-CD73-4EB5-8390-8653BE590246}):41"/>
+				<v:ud v:nameU="visLegendShape" v:prompt="" v:val="VT0(2):26"/>
+			</v:userDefs>
+			<title>用户</title>
+			<desc>User1 Wants:APP1</desc>
+			<g id="shape25-110" v:mID="25" v:groupContext="shape" transform="translate(18.0575,0)">
+				<title>工作表.25</title>
+				<path d="M26.29 533.22 A8.81 8.81 -180 1 0 8.67 533.22 A8.81 8.81 -180 1 0 26.29 533.22 ZM27.58 544.41 L7.17 544.41
+							 C3.22 544.41 0 547.62 0 551.58 L0 576.58 L5.59 576.58 L5.59 562.03 L7.45 562.03 L7.45 595.28 L16.55
+							 595.28 L16.55 580.42 C16.55 579.9 16.97 579.48 17.48 579.48 C18 579.48 18.42 579.9 18.42 580.42 L18.42
+							 595.28 L28.04 595.28 L28.04 561.98 L29.91 561.98 L29.91 576.58 L34.75 576.58 L34.75 551.58 C34.75 547.62
+							 31.54 544.41 27.58 544.41 Z" class="st1"/>
+			</g>
+			<g id="shape24-112" v:mID="24" v:groupContext="groupContent">
+				<v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
+				<v:textRect cx="35.4331" cy="620.877" width="102.19" height="51.2036"/>
+				<text x="13.96" y="615.48" class="st3" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>User1<v:newlineChar/><tspan
+							x="-9.62" dy="1.2em" class="st6">Wants:APP1</tspan></text>			</g>
+		</g>
+		<g id="group26-115" transform="translate(49.6063,-311.811)" v:mID="26" v:groupContext="group">
+			<v:custProps>
+				<v:cp v:nameU="Name" v:lbl="名称" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Location" v:lbl="位置" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Building" v:lbl="构建" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Room" v:lbl="空间" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Department" v:lbl="部门" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="ShapeClass" v:lbl="ShapeClass" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true"
+						v:ask="false" v:langID="2052" v:cal="0" v:val="VT4(连接性)"/>
+				<v:cp v:nameU="ShapeType" v:lbl="ShapeType" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true"
+						v:ask="false" v:langID="2052" v:cal="0" v:val="VT4(用户)"/>
+			</v:custProps>
+			<v:userDefs>
+				<v:ud v:nameU="visVersion" v:prompt="" v:val="VT0(15):26"/>
+				<v:ud v:nameU="ShapeClass" v:prompt="" v:val="VT0(5):26"/>
+				<v:ud v:nameU="SolSH" v:prompt="" v:val="VT15({BF0433D9-CD73-4EB5-8390-8653BE590246}):41"/>
+				<v:ud v:nameU="visLegendShape" v:prompt="" v:val="VT0(2):26"/>
+			</v:userDefs>
+			<title>用户.26</title>
+			<desc>User2 Wants:APP2</desc>
+			<g id="shape27-116" v:mID="27" v:groupContext="shape" transform="translate(18.0575,0)">
+				<title>工作表.27</title>
+				<path d="M26.29 533.22 A8.81 8.81 -180 1 0 8.67 533.22 A8.81 8.81 -180 1 0 26.29 533.22 ZM27.58 544.41 L7.17 544.41
+							 C3.22 544.41 0 547.62 0 551.58 L0 576.58 L5.59 576.58 L5.59 562.03 L7.45 562.03 L7.45 595.28 L16.55
+							 595.28 L16.55 580.42 C16.55 579.9 16.97 579.48 17.48 579.48 C18 579.48 18.42 579.9 18.42 580.42 L18.42
+							 595.28 L28.04 595.28 L28.04 561.98 L29.91 561.98 L29.91 576.58 L34.75 576.58 L34.75 551.58 C34.75 547.62
+							 31.54 544.41 27.58 544.41 Z" class="st1"/>
+			</g>
+			<g id="shape26-118" v:mID="26" v:groupContext="groupContent">
+				<v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
+				<v:textRect cx="35.4331" cy="620.877" width="102.19" height="51.2036"/>
+				<text x="13.96" y="615.48" class="st3" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>User2<v:newlineChar/><tspan
+							x="-9.62" dy="1.2em" class="st6">Wants:APP2</tspan></text>			</g>
+		</g>
+		<g id="group28-121" transform="translate(49.6063,-127.559)" v:mID="28" v:groupContext="group">
+			<v:custProps>
+				<v:cp v:nameU="Name" v:lbl="名称" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Location" v:lbl="位置" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Building" v:lbl="构建" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Room" v:lbl="空间" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="Department" v:lbl="部门" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="false" v:ask="false"
+						v:langID="2052" v:cal="0"/>
+				<v:cp v:nameU="ShapeClass" v:lbl="ShapeClass" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true"
+						v:ask="false" v:langID="2052" v:cal="0" v:val="VT4(连接性)"/>
+				<v:cp v:nameU="ShapeType" v:lbl="ShapeType" v:prompt="" v:type="0" v:format="" v:sortKey="" v:invis="true"
+						v:ask="false" v:langID="2052" v:cal="0" v:val="VT4(用户)"/>
+			</v:custProps>
+			<v:userDefs>
+				<v:ud v:nameU="visVersion" v:prompt="" v:val="VT0(15):26"/>
+				<v:ud v:nameU="ShapeClass" v:prompt="" v:val="VT0(5):26"/>
+				<v:ud v:nameU="SolSH" v:prompt="" v:val="VT15({BF0433D9-CD73-4EB5-8390-8653BE590246}):41"/>
+				<v:ud v:nameU="visLegendShape" v:prompt="" v:val="VT0(2):26"/>
+			</v:userDefs>
+			<title>用户.28</title>
+			<desc>User3 Wants:APP3</desc>
+			<g id="shape29-122" v:mID="29" v:groupContext="shape" transform="translate(18.0575,0)">
+				<title>工作表.29</title>
+				<path d="M26.29 533.22 A8.81 8.81 -180 1 0 8.67 533.22 A8.81 8.81 -180 1 0 26.29 533.22 ZM27.58 544.41 L7.17 544.41
+							 C3.22 544.41 0 547.62 0 551.58 L0 576.58 L5.59 576.58 L5.59 562.03 L7.45 562.03 L7.45 595.28 L16.55
+							 595.28 L16.55 580.42 C16.55 579.9 16.97 579.48 17.48 579.48 C18 579.48 18.42 579.9 18.42 580.42 L18.42
+							 595.28 L28.04 595.28 L28.04 561.98 L29.91 561.98 L29.91 576.58 L34.75 576.58 L34.75 551.58 C34.75 547.62
+							 31.54 544.41 27.58 544.41 Z" class="st1"/>
+			</g>
+			<g id="shape28-124" v:mID="28" v:groupContext="groupContent">
+				<v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
+				<v:textRect cx="35.4331" cy="620.877" width="102.19" height="51.2036"/>
+				<text x="13.96" y="615.48" class="st3" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>User3<v:newlineChar/><tspan
+							x="-9.62" dy="1.2em" class="st6">Wants:APP3</tspan></text>			</g>
+		</g>
+		<g id="shape1003-127" v:mID="1003" v:groupContext="shape" v:layerMember="0" transform="translate(99.8466,-514.757)">
+			<title>动态连接线.1003</title>
+			<desc>-&#62;8003 Multi Conn</desc>
+			<v:userDefs>
+				<v:ud v:nameU="visVersion" v:prompt="" v:val="VT0(15):26"/>
+			</v:userDefs>
+			<v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
+			<v:textRect cx="56.059" cy="684.836" width="93.29" height="51.2036"/>
+			<path d="M5.09 601.03 L5.33 601.3 L113.11 723.13" class="st10"/>
+			<rect v:rectContext="textBkgnd" x="15.4535" y="663.236" width="81.2109" height="43.1999" class="st12"/>
+			<text x="30.58" y="679.44" class="st13" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>-&#62;8003<v:newlineChar/><tspan
+						x="15.45" dy="1.2em" class="st6">Multi Conn</tspan></text>		</g>
+		<g id="shape1004-139" v:mID="1004" v:groupContext="shape" v:layerMember="0" transform="translate(102.415,-340.157)">
+			<title>动态连接线.1004</title>
+			<desc>-&#62;8004 Multi Conn</desc>
+			<v:userDefs>
+				<v:ud v:nameU="visVersion" v:prompt="" v:val="VT0(15):26"/>
+			</v:userDefs>
+			<v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
+			<v:textRect cx="57.9325" cy="588.189" width="93.29" height="51.2036"/>
+			<path d="M7.68 588.19 L8.04 588.19 L107.83 588.19" class="st10"/>
+			<rect v:rectContext="textBkgnd" x="17.327" y="566.589" width="81.2109" height="43.1999" class="st12"/>
+			<text x="32.45" y="582.79" class="st13" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>-&#62;8004<v:newlineChar/><tspan
+						x="17.33" dy="1.2em" class="st6">Multi Conn</tspan></text>		</g>
+		<g id="shape1005-149" v:mID="1005" v:groupContext="shape" v:layerMember="0" transform="translate(98.1493,-177.812)">
+			<title>动态连接线.1005</title>
+			<desc>-&#62;8005 Multi Conn</desc>
+			<v:userDefs>
+				<v:ud v:nameU="visVersion" v:prompt="" v:val="VT0(15):26"/>
+			</v:userDefs>
+			<v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
+			<v:textRect cx="60.0654" cy="527.376" width="93.29" height="51.2036"/>
+			<path d="M5.09 589.52 L5.33 589.25 L114.8 465.5" class="st10"/>
+			<rect v:rectContext="textBkgnd" x="19.4599" y="505.776" width="81.2109" height="43.1999" class="st12"/>
+			<text x="34.58" y="521.98" class="st13" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>-&#62;8005<v:newlineChar/><tspan
+						x="19.46" dy="1.2em" class="st6">Multi Conn</tspan></text>		</g>
+		<g id="shape1006-159" v:mID="1006" v:groupContext="shape" v:layerMember="0" transform="translate(277.783,-354.331)">
+			<title>动态连接线.1006</title>
+			<desc>NPS &#38; NPC Multiplexing Connection TCP or KCP Only One Conn Pe...</desc>
+			<v:userDefs>
+				<v:ud v:nameU="visVersion" v:prompt="" v:val="VT0(15):26"/>
+			</v:userDefs>
+			<v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
+			<v:textRect cx="70.4362" cy="602.362" width="103.62" height="159.204"/>
+			<path d="M7.68 602.36 L8.04 602.36 L132.83 602.36" class="st10"/>
+			<rect v:rectContext="textBkgnd" x="24.6671" y="526.762" width="91.5381" height="151.2" class="st12"/>
+			<text x="30.38" y="542.96" class="st13" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>NPS &#38; NPC<v:newlineChar/><tspan
+						x="24.67" dy="1.2em" class="st6">Multiplexing<v:newlineChar/></tspan><tspan x="28.6" dy="1.2em" class="st6">Connection<v:newlineChar/></tspan><tspan
+						x="30.53" dy="1.2em" class="st6">TCP or KCP<v:newlineChar/></tspan><tspan x="36.41" dy="1.2em" class="st6">Only One<v:newlineChar/></tspan><tspan
+						x="32.66" dy="1.2em" class="st6">Conn Peer<v:newlineChar/></tspan><tspan x="55.18" dy="1.2em" class="st6">NPC</tspan></text>		</g>
+		<g id="shape1007-174" v:mID="1007" v:groupContext="shape" v:layerMember="0" transform="translate(603.767,-380.876)">
+			<title>动态连接线.1007</title>
+			<desc>-&#62;PORT Multi Conn</desc>
+			<v:userDefs>
+				<v:ud v:nameU="visVersion" v:prompt="" v:val="VT0(15):26"/>
+			</v:userDefs>
+			<v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
+			<v:textRect cx="55.0044" cy="550.955" width="93.29" height="51.2036"/>
+			<path d="M5.09 589.52 L5.33 589.25 L105.29 476.25" class="st10"/>
+			<rect v:rectContext="textBkgnd" x="14.3989" y="529.355" width="81.2109" height="43.1999" class="st12"/>
+			<text x="27.89" y="545.56" class="st13" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>-&#62;PORT<v:newlineChar/><tspan
+						x="14.4" dy="1.2em" class="st6">Multi Conn</tspan></text>		</g>
+		<g id="shape1008-184" v:mID="1008" v:groupContext="shape" v:layerMember="0" transform="translate(603.767,-340.157)">
+			<title>动态连接线.1008</title>
+			<desc>-&#62;PORT Multi Conn</desc>
+			<v:userDefs>
+				<v:ud v:nameU="visVersion" v:prompt="" v:val="VT0(15):26"/>
+			</v:userDefs>
+			<v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
+			<v:textRect cx="55.3104" cy="588.189" width="93.29" height="51.2036"/>
+			<path d="M7.68 588.19 L8.04 588.19 L102.58 588.19" class="st10"/>
+			<rect v:rectContext="textBkgnd" x="14.7049" y="566.589" width="81.2109" height="43.1999" class="st12"/>
+			<text x="28.19" y="582.79" class="st13" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>-&#62;PORT<v:newlineChar/><tspan
+						x="14.7" dy="1.2em" class="st6">Multi Conn</tspan></text>		</g>
+		<g id="shape1009-194" v:mID="1009" v:groupContext="shape" v:layerMember="0" transform="translate(603.767,-313.612)">
+			<title>动态连接线.1009</title>
+			<desc>-&#62;PORT Multi Conn</desc>
+			<v:userDefs>
+				<v:ud v:nameU="visVersion" v:prompt="" v:val="VT0(15):26"/>
+			</v:userDefs>
+			<v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
+			<v:textRect cx="68.0438" cy="667.943" width="93.29" height="51.2036"/>
+			<path d="M5.09 601.03 L5.33 601.3 L105.29 714.3" class="st10"/>
+			<rect v:rectContext="textBkgnd" x="27.4383" y="646.343" width="81.2109" height="43.1999" class="st12"/>
+			<text x="40.93" y="662.54" class="st13" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>-&#62;PORT<v:newlineChar/><tspan
+						x="27.44" dy="1.2em" class="st6">Multi Conn</tspan></text>		</g>
+		<g id="shape1010-204" v:mID="1010" v:groupContext="shape" v:layerMember="0" transform="translate(488.431,-340.157)">
+			<title>动态连接线.1010</title>
+			<desc>NPS &#38; NPC</desc>
+			<v:userDefs>
+				<v:ud v:nameU="visVersion" v:prompt="" v:val="VT0(15):26"/>
+			</v:userDefs>
+			<v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
+			<v:textRect cx="27.9165" cy="588.189" width="90" height="72.8036"/>
+			<path d="M7.68 588.19 L8.04 588.19 L47.79 588.19" class="st10"/>
+			<rect v:rectContext="textBkgnd" x="12.6587" y="555.789" width="30.5156" height="64.7998" class="st12"/>
+			<text x="13.32" y="571.99" class="st13" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>NPS<v:newlineChar/><tspan
+						x="21.78" dy="1.2em" class="st6">&#38;<v:newlineChar/></tspan><tspan x="12.66" dy="1.2em" class="st6">NPC</tspan></text>		</g>
+		<g id="shape1011-215" v:mID="1011" v:groupContext="shape" transform="translate(34.0157,-62.8844)">
+			<title>工作表.1011</title>
+			<path d="M0 595.28 L398.27 595.28 L398.27 85.04 L0 85.04 L0 595.28 Z" class="st14"/>
+		</g>
+		<g id="shape1012-217" v:mID="1012" v:groupContext="shape" transform="translate(473.386,-62.8844)">
+			<title>工作表.1012</title>
+			<path d="M0 595.28 L320.31 595.28 L320.31 85.04 L0 85.04 L0 595.28 Z" class="st14"/>
+		</g>
+		<g id="shape1013-219" v:mID="1013" v:groupContext="shape" transform="translate(255.118,-496.063)">
+			<title>工作表.1013</title>
+			<desc>Internet</desc>
+			<v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
+			<v:textRect cx="63.7795" cy="559.843" width="127.56" height="70.8661"/>
+			<rect x="0" y="524.409" width="127.559" height="70.8661" class="st15"/>
+			<text x="23.98" y="567.04" class="st16" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>Internet</text>		</g>
+		<g id="shape1014-222" v:mID="1014" v:groupContext="shape" transform="translate(517.323,-515.866)">
+			<title>工作表.1014</title>
+			<desc>Intranet</desc>
+			<v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
+			<v:textRect cx="76" cy="579.646" width="152.01" height="31.2598"/>
+			<rect x="0" y="564.016" width="152" height="31.2598" class="st15"/>
+			<text x="36.43" y="586.85" class="st16" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>Intranet</text>		</g>
+	</g>
+</svg>

+ 2 - 2
lib/common/const.go

@@ -49,6 +49,6 @@ const (
 	MUX_PING_RETURN
 	MUX_PING             int32 = -1
 	MAXIMUM_SEGMENT_SIZE       = PoolSizeWindow
-	MAXIMUM_WINDOW_SIZE        = 1 << 25 // 1<<31-1 TCP slide window size is very large,
-	// we use 32M, reduce memory usage
+	MAXIMUM_WINDOW_SIZE        = 1 << 27 // 1<<31-1 TCP slide window size is very large,
+	// we use 128M, reduce memory usage
 )

+ 48 - 0
lib/common/logs.go

@@ -0,0 +1,48 @@
+package common
+
+import (
+	"github.com/astaxie/beego/logs"
+	"time"
+)
+
+const MaxMsgLen = 5000
+
+var logMsgs string
+
+func init() {
+	logs.Register("store", func() logs.Logger {
+		return new(StoreMsg)
+	})
+}
+
+func GetLogMsg() string {
+	return logMsgs
+}
+
+type StoreMsg struct {
+}
+
+func (lg *StoreMsg) Init(config string) error {
+	return nil
+}
+
+func (lg *StoreMsg) WriteMsg(when time.Time, msg string, level int) error {
+	m := when.Format("2006-01-02 15:04:05") + " " + msg + "\r\n"
+	if len(logMsgs) > MaxMsgLen {
+		start := MaxMsgLen - len(m)
+		if start <= 0 {
+			start = MaxMsgLen
+		}
+		logMsgs = logMsgs[start:]
+	}
+	logMsgs += m
+	return nil
+}
+
+func (lg *StoreMsg) Destroy() {
+	return
+}
+
+func (lg *StoreMsg) Flush() {
+	return
+}

+ 230 - 28
lib/common/netpackager.go

@@ -6,6 +6,9 @@ import (
 	"encoding/json"
 	"errors"
 	"io"
+	"io/ioutil"
+	"net"
+	"strconv"
 	"strings"
 )
 
@@ -38,6 +41,9 @@ func (Self *BasePackager) NewPac(contents ...interface{}) (err error) {
 		}
 	}
 	Self.setLength()
+	if Self.Length > MAXIMUM_SEGMENT_SIZE {
+		err = errors.New("mux:packer: newpack content segment too large")
+	}
 	return
 }
 
@@ -74,6 +80,11 @@ func (Self *BasePackager) UnPack(reader io.Reader) (n uint16, err error) {
 	}
 	if int(Self.Length) > cap(Self.Content) {
 		err = errors.New("unpack err, content length too large")
+		return
+	}
+	if Self.Length > MAXIMUM_SEGMENT_SIZE {
+		err = errors.New("mux:packer: unpack content segment too large")
+		return
 	}
 	Self.Content = Self.Content[:int(Self.Length)]
 	//n, err := io.ReadFull(reader, Self.Content)
@@ -119,7 +130,8 @@ func (Self *BasePackager) Split() (strList []string) {
 	return
 }
 
-type ConnPackager struct { // Todo
+type ConnPackager struct {
+	// Todo
 	ConnType uint8
 	BasePackager
 }
@@ -150,10 +162,9 @@ func (Self *ConnPackager) UnPack(reader io.Reader) (n uint16, err error) {
 }
 
 type MuxPackager struct {
-	Flag       uint8
-	Id         int32
-	Window     uint32
-	ReadLength uint32
+	Flag   uint8
+	Id     int32
+	Window uint64
 	BasePackager
 }
 
@@ -166,19 +177,8 @@ func (Self *MuxPackager) NewPac(flag uint8, id int32, content ...interface{}) (e
 		err = Self.BasePackager.NewPac(content...)
 		//logs.Warn(Self.Length, string(Self.Content))
 	case MUX_MSG_SEND_OK:
-		// MUX_MSG_SEND_OK contains two data
-		switch content[0].(type) {
-		case int:
-			Self.Window = uint32(content[0].(int))
-		case uint32:
-			Self.Window = content[0].(uint32)
-		}
-		switch content[1].(type) {
-		case int:
-			Self.ReadLength = uint32(content[1].(int))
-		case uint32:
-			Self.ReadLength = content[1].(uint32)
-		}
+		// MUX_MSG_SEND_OK contains one data
+		Self.Window = content[0].(uint64)
 	}
 	return
 }
@@ -198,10 +198,6 @@ func (Self *MuxPackager) Pack(writer io.Writer) (err error) {
 		WindowBuff.Put(Self.Content)
 	case MUX_MSG_SEND_OK:
 		err = binary.Write(writer, binary.LittleEndian, Self.Window)
-		if err != nil {
-			return
-		}
-		err = binary.Write(writer, binary.LittleEndian, Self.ReadLength)
 	}
 	return
 }
@@ -223,13 +219,219 @@ func (Self *MuxPackager) UnPack(reader io.Reader) (n uint16, err error) {
 		//logs.Warn("unpack", Self.Length, string(Self.Content))
 	case MUX_MSG_SEND_OK:
 		err = binary.Read(reader, binary.LittleEndian, &Self.Window)
-		if err != nil {
-			return
-		}
-		n += 4 // uint32
-		err = binary.Read(reader, binary.LittleEndian, &Self.ReadLength)
-		n += 4 // uint32
+		n += 8 // uint64
 	}
 	n += 5 //uint8 int32
 	return
 }
+
+func (Self *MuxPackager) reset() {
+	Self.Id = 0
+	Self.Flag = 0
+	Self.Length = 0
+	Self.Content = nil
+	Self.Window = 0
+}
+
+const (
+	ipV4       = 1
+	domainName = 3
+	ipV6       = 4
+)
+
+type UDPHeader struct {
+	Rsv  uint16
+	Frag uint8
+	Addr *Addr
+}
+
+func NewUDPHeader(rsv uint16, frag uint8, addr *Addr) *UDPHeader {
+	return &UDPHeader{
+		Rsv:  rsv,
+		Frag: frag,
+		Addr: addr,
+	}
+}
+
+type Addr struct {
+	Type uint8
+	Host string
+	Port uint16
+}
+
+func (addr *Addr) String() string {
+	return net.JoinHostPort(addr.Host, strconv.Itoa(int(addr.Port)))
+}
+
+func (addr *Addr) Decode(b []byte) error {
+	addr.Type = b[0]
+	pos := 1
+	switch addr.Type {
+	case ipV4:
+		addr.Host = net.IP(b[pos : pos+net.IPv4len]).String()
+		pos += net.IPv4len
+	case ipV6:
+		addr.Host = net.IP(b[pos : pos+net.IPv6len]).String()
+		pos += net.IPv6len
+	case domainName:
+		addrlen := int(b[pos])
+		pos++
+		addr.Host = string(b[pos : pos+addrlen])
+		pos += addrlen
+	default:
+		return errors.New("decode error")
+	}
+
+	addr.Port = binary.BigEndian.Uint16(b[pos:])
+
+	return nil
+}
+
+func (addr *Addr) Encode(b []byte) (int, error) {
+	b[0] = addr.Type
+	pos := 1
+	switch addr.Type {
+	case ipV4:
+		ip4 := net.ParseIP(addr.Host).To4()
+		if ip4 == nil {
+			ip4 = net.IPv4zero.To4()
+		}
+		pos += copy(b[pos:], ip4)
+	case domainName:
+		b[pos] = byte(len(addr.Host))
+		pos++
+		pos += copy(b[pos:], []byte(addr.Host))
+	case ipV6:
+		ip16 := net.ParseIP(addr.Host).To16()
+		if ip16 == nil {
+			ip16 = net.IPv6zero.To16()
+		}
+		pos += copy(b[pos:], ip16)
+	default:
+		b[0] = ipV4
+		copy(b[pos:pos+4], net.IPv4zero.To4())
+		pos += 4
+	}
+	binary.BigEndian.PutUint16(b[pos:], addr.Port)
+	pos += 2
+
+	return pos, nil
+}
+
+func (h *UDPHeader) Write(w io.Writer) error {
+	b := BufPoolUdp.Get().([]byte)
+	defer BufPoolUdp.Put(b)
+
+	binary.BigEndian.PutUint16(b[:2], h.Rsv)
+	b[2] = h.Frag
+
+	addr := h.Addr
+	if addr == nil {
+		addr = &Addr{}
+	}
+	length, _ := addr.Encode(b[3:])
+
+	_, err := w.Write(b[:3+length])
+	return err
+}
+
+type UDPDatagram struct {
+	Header *UDPHeader
+	Data   []byte
+}
+
+func ReadUDPDatagram(r io.Reader) (*UDPDatagram, error) {
+	b := BufPoolUdp.Get().([]byte)
+	defer BufPoolUdp.Put(b)
+
+	// when r is a streaming (such as TCP connection), we may read more than the required data,
+	// but we don't know how to handle it. So we use io.ReadFull to instead of io.ReadAtLeast
+	// to make sure that no redundant data will be discarded.
+	n, err := io.ReadFull(r, b[:5])
+	if err != nil {
+		return nil, err
+	}
+
+	header := &UDPHeader{
+		Rsv:  binary.BigEndian.Uint16(b[:2]),
+		Frag: b[2],
+	}
+
+	atype := b[3]
+	hlen := 0
+	switch atype {
+	case ipV4:
+		hlen = 10
+	case ipV6:
+		hlen = 22
+	case domainName:
+		hlen = 7 + int(b[4])
+	default:
+		return nil, errors.New("addr not support")
+	}
+	dlen := int(header.Rsv)
+	if dlen == 0 { // standard SOCKS5 UDP datagram
+		extra, err := ioutil.ReadAll(r) // we assume no redundant data
+		if err != nil {
+			return nil, err
+		}
+		copy(b[n:], extra)
+		n += len(extra) // total length
+		dlen = n - hlen // data length
+	} else { // extended feature, for UDP over TCP, using reserved field as data length
+		if _, err := io.ReadFull(r, b[n:hlen+dlen]); err != nil {
+			return nil, err
+		}
+		n = hlen + dlen
+	}
+	header.Addr = new(Addr)
+	if err := header.Addr.Decode(b[3:hlen]); err != nil {
+		return nil, err
+	}
+	data := make([]byte, dlen)
+	copy(data, b[hlen:n])
+	d := &UDPDatagram{
+		Header: header,
+		Data:   data,
+	}
+	return d, nil
+}
+
+func NewUDPDatagram(header *UDPHeader, data []byte) *UDPDatagram {
+	return &UDPDatagram{
+		Header: header,
+		Data:   data,
+	}
+}
+
+func (d *UDPDatagram) Write(w io.Writer) error {
+	h := d.Header
+	if h == nil {
+		h = &UDPHeader{}
+	}
+	buf := bytes.Buffer{}
+	if err := h.Write(&buf); err != nil {
+		return err
+	}
+	if _, err := buf.Write(d.Data); err != nil {
+		return err
+	}
+
+	_, err := buf.WriteTo(w)
+	return err
+}
+
+func ToSocksAddr(addr net.Addr) *Addr {
+	host := "0.0.0.0"
+	port := 0
+	if addr != nil {
+		h, p, _ := net.SplitHostPort(addr.String())
+		host = h
+		port, _ = strconv.Atoi(p)
+	}
+	return &Addr{
+		Type: ipV4,
+		Host: host,
+		Port: uint16(port),
+	}
+}

+ 7 - 4
lib/common/pool.go

@@ -7,7 +7,7 @@ import (
 
 const PoolSize = 64 * 1024
 const PoolSizeSmall = 100
-const PoolSizeUdp = 1472
+const PoolSizeUdp = 1472 + 200
 const PoolSizeCopy = 32 << 10
 const PoolSizeBuffer = 4096
 const PoolSizeWindow = PoolSizeBuffer - 2 - 4 - 4 - 1
@@ -93,18 +93,20 @@ type windowBufferPool struct {
 func (Self *windowBufferPool) New() {
 	Self.pool = sync.Pool{
 		New: func() interface{} {
-			return make([]byte, PoolSizeWindow, PoolSizeWindow)
+			return make([]byte, PoolSizeWindow)
 		},
 	}
 }
 
 func (Self *windowBufferPool) Get() (buf []byte) {
 	buf = Self.pool.Get().([]byte)
-	return buf[:PoolSizeWindow]
+	buf = buf[:PoolSizeWindow]
+	return buf
 }
 
 func (Self *windowBufferPool) Put(x []byte) {
-	Self.pool.Put(x[:PoolSizeWindow]) // make buf to full
+	x = x[:0] // clean buf
+	Self.pool.Put(x)
 }
 
 type bufferPool struct {
@@ -146,6 +148,7 @@ func (Self *muxPackagerPool) Get() *MuxPackager {
 }
 
 func (Self *muxPackagerPool) Put(pack *MuxPackager) {
+	pack.reset()
 	Self.pool.Put(pack)
 }
 

+ 13 - 2
lib/common/run.go

@@ -48,9 +48,20 @@ func IsWindows() bool {
 func GetLogPath() string {
 	var path string
 	if IsWindows() {
-		path = GetAppPath()
+		path = filepath.Join(GetAppPath(), "nps.log")
 	} else {
-		path = "/tmp"
+		path = "/var/log/nps.log"
+	}
+	return path
+}
+
+//interface npc log file path
+func GetNpcLogPath() string {
+	var path string
+	if IsWindows() {
+		path = filepath.Join(GetAppPath(), "npc.log")
+	} else {
+		path = "/var/log/npc.log"
 	}
 	return path
 }

+ 68 - 4
lib/common/util.go

@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"encoding/base64"
 	"encoding/binary"
+	"errors"
 	"html/template"
 	"io"
 	"io/ioutil"
@@ -15,7 +16,7 @@ import (
 	"strings"
 	"sync"
 
-	"github.com/cnlh/nps/lib/crypt"
+	"ehang.io/nps/lib/crypt"
 )
 
 //Get the corresponding IP address through domain name
@@ -50,7 +51,10 @@ func DomainCheck(domain string) bool {
 func CheckAuth(r *http.Request, user, passwd string) bool {
 	s := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
 	if len(s) != 2 {
-		return false
+		s = strings.SplitN(r.Header.Get("Proxy-Authorization"), " ", 2)
+		if len(s) != 2 {
+			return false
+		}
 	}
 
 	b, err := base64.StdEncoding.DecodeString(s[1])
@@ -109,8 +113,8 @@ func ChangeHostAndHeader(r *http.Request, host string, header string, addr strin
 	}
 	addr = strings.Split(addr, ":")[0]
 	if prior, ok := r.Header["X-Forwarded-For"]; ok {
-    		addr = strings.Join(prior, ", ") + ", " + addr
-    	}
+		addr = strings.Join(prior, ", ") + ", " + addr
+	}
 	r.Header.Set("X-Forwarded-For", addr)
 	r.Header.Set("X-Real-IP", addr)
 }
@@ -121,6 +125,7 @@ func ReadAllFromFile(filePath string) ([]byte, error) {
 	if err != nil {
 		return nil, err
 	}
+	defer f.Close()
 	return ioutil.ReadAll(f)
 }
 
@@ -395,3 +400,62 @@ func GetExtFromPath(path string) string {
 	}
 	return string(re.Find([]byte(s[0])))
 }
+
+var externalIp string
+
+func GetExternalIp() string {
+	if externalIp != "" {
+		return externalIp
+	}
+	resp, err := http.Get("http://myexternalip.com/raw")
+	if err != nil {
+		return ""
+	}
+	defer resp.Body.Close()
+	content, _ := ioutil.ReadAll(resp.Body)
+	externalIp = string(content)
+	return externalIp
+}
+
+func GetIntranetIp() (error, string) {
+	addrs, err := net.InterfaceAddrs()
+	if err != nil {
+		return nil, ""
+	}
+	for _, address := range addrs {
+		// 检查ip地址判断是否回环地址
+		if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
+			if ipnet.IP.To4() != nil {
+				return nil, ipnet.IP.To4().String()
+			}
+		}
+	}
+	return errors.New("get intranet ip error"), ""
+}
+
+func IsPublicIP(IP net.IP) bool {
+	if IP.IsLoopback() || IP.IsLinkLocalMulticast() || IP.IsLinkLocalUnicast() {
+		return false
+	}
+	if ip4 := IP.To4(); ip4 != nil {
+		switch true {
+		case ip4[0] == 10:
+			return false
+		case ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31:
+			return false
+		case ip4[0] == 192 && ip4[1] == 168:
+			return false
+		default:
+			return true
+		}
+	}
+	return false
+}
+
+func GetServerIpByClientIp(clientIp net.IP) string {
+	if IsPublicIP(clientIp) {
+		return GetExternalIp()
+	}
+	_, ip := GetIntranetIp()
+	return ip
+}

+ 9 - 7
lib/config/config.go

@@ -6,8 +6,8 @@ import (
 	"regexp"
 	"strings"
 
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/file"
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/file"
 )
 
 type CommonConfig struct {
@@ -241,13 +241,15 @@ func dealTunnel(s string) *file.Tunnel {
 			t.StripPre = item[1]
 		case "multi_account":
 			t.MultiAccount = &file.MultiAccount{}
-			if b, err := common.ReadAllFromFile(item[1]); err != nil {
-				panic(err)
-			} else {
-				if content, err := common.ParseStr(string(b)); err != nil {
+			if common.FileExists(item[1]) {
+				if b, err := common.ReadAllFromFile(item[1]); err != nil {
 					panic(err)
 				} else {
-					t.MultiAccount.AccountMap = dealMultiUser(content)
+					if content, err := common.ParseStr(string(b)); err != nil {
+						panic(err)
+					} else {
+						t.MultiAccount.AccountMap = dealMultiUser(content)
+					}
 				}
 			}
 		}

+ 9 - 9
lib/conn/conn.go

@@ -3,11 +3,11 @@ package conn
 import (
 	"bufio"
 	"bytes"
+	"ehang.io/nps/lib/goroutine"
 	"encoding/binary"
 	"encoding/json"
 	"errors"
 	"github.com/astaxie/beego/logs"
-	"github.com/cnlh/nps/lib/goroutine"
 	"io"
 	"net"
 	"net/http"
@@ -16,11 +16,11 @@ import (
 	"strings"
 	"time"
 
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/crypt"
-	"github.com/cnlh/nps/lib/file"
-	"github.com/cnlh/nps/lib/mux"
-	"github.com/cnlh/nps/lib/rate"
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/crypt"
+	"ehang.io/nps/lib/file"
+	"ehang.io/nps/lib/mux"
+	"ehang.io/nps/lib/rate"
 	"github.com/xtaci/kcp-go"
 )
 
@@ -87,7 +87,7 @@ func (s *Conn) GetShortContent(l int) (b []byte, err error) {
 
 //读取指定长度内容
 func (s *Conn) ReadLen(cLen int, buf []byte) (int, error) {
-	if cLen > len(buf) {
+	if cLen > len(buf) || cLen <= 0 {
 		return 0, errors.New("长度错误" + strconv.Itoa(cLen))
 	}
 	if n, err := io.ReadFull(s, buf[:cLen]); err != nil || n != cLen {
@@ -124,8 +124,8 @@ func (s *Conn) SetAlive(tp string) {
 	case *net.TCPConn:
 		conn := s.Conn.(*net.TCPConn)
 		conn.SetReadDeadline(time.Time{})
-		conn.SetKeepAlive(true)
-		conn.SetKeepAlivePeriod(time.Duration(2 * time.Second))
+		//conn.SetKeepAlive(false)
+		//conn.SetKeepAlivePeriod(time.Duration(2 * time.Second))
 	case *mux.PortConn:
 		s.Conn.(*mux.PortConn).SetReadDeadline(time.Time{})
 	}

+ 31 - 1
lib/conn/link.go

@@ -1,5 +1,7 @@
 package conn
 
+import "time"
+
 type Secret struct {
 	Password string
 	Conn     *Conn
@@ -19,9 +21,20 @@ type Link struct {
 	Compress   bool
 	LocalProxy bool
 	RemoteAddr string
+	Option     Options
+}
+
+type Option func(*Options)
+
+type Options struct {
+	Timeout time.Duration
 }
 
-func NewLink(connType string, host string, crypt bool, compress bool, remoteAddr string, localProxy bool) *Link {
+var defaultTimeOut = time.Second * 5
+
+func NewLink(connType string, host string, crypt bool, compress bool, remoteAddr string, localProxy bool, opts ...Option) *Link {
+	options := newOptions(opts...)
+
 	return &Link{
 		RemoteAddr: remoteAddr,
 		ConnType:   connType,
@@ -29,5 +42,22 @@ func NewLink(connType string, host string, crypt bool, compress bool, remoteAddr
 		Crypt:      crypt,
 		Compress:   compress,
 		LocalProxy: localProxy,
+		Option:     options,
+	}
+}
+
+func newOptions(opts ...Option) Options {
+	opt := Options{
+		Timeout: defaultTimeOut,
+	}
+	for _, o := range opts {
+		o(&opt)
+	}
+	return opt
+}
+
+func LinkTimeout(t time.Duration) Option {
+	return func(opt *Options) {
+		opt.Timeout = t
 	}
 }

+ 1 - 8
lib/conn/snappy.go

@@ -3,7 +3,6 @@ package conn
 import (
 	"io"
 
-	"github.com/cnlh/nps/lib/common"
 	"github.com/golang/snappy"
 )
 
@@ -32,13 +31,7 @@ func (s *SnappyConn) Write(b []byte) (n int, err error) {
 
 //snappy压缩读
 func (s *SnappyConn) Read(b []byte) (n int, err error) {
-	buf := common.BufPool.Get().([]byte)
-	defer common.BufPool.Put(buf)
-	if n, err = s.r.Read(buf); err != nil {
-		return
-	}
-	copy(b, buf[:n])
-	return
+	return s.r.Read(b)
 }
 
 func (s *SnappyConn) Close() error {

+ 1 - 1
lib/daemon/daemon.go

@@ -9,7 +9,7 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/cnlh/nps/lib/common"
+	"ehang.io/nps/lib/common"
 )
 
 func InitDaemon(f string, runPath string, pidPath string) {

+ 1 - 1
lib/daemon/reload.go

@@ -8,8 +8,8 @@ import (
 	"path/filepath"
 	"syscall"
 
+	"ehang.io/nps/lib/common"
 	"github.com/astaxie/beego"
-	"github.com/cnlh/nps/lib/common"
 )
 
 func init() {

+ 3 - 3
lib/file/db.go

@@ -9,9 +9,9 @@ import (
 	"strings"
 	"sync"
 
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/crypt"
-	"github.com/cnlh/nps/lib/rate"
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/crypt"
+	"ehang.io/nps/lib/rate"
 )
 
 type DbUtils struct {

+ 2 - 2
lib/file/file.go

@@ -9,8 +9,8 @@ import (
 	"sync"
 	"sync/atomic"
 
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/rate"
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/rate"
 )
 
 func NewJsonDb(runPath string) *JsonDb {

+ 2 - 1
lib/file/obj.go

@@ -6,7 +6,7 @@ import (
 	"sync/atomic"
 	"time"
 
-	"github.com/cnlh/nps/lib/rate"
+	"ehang.io/nps/lib/rate"
 	"github.com/pkg/errors"
 )
 
@@ -50,6 +50,7 @@ type Client struct {
 	WebPassword     string     //the password of web login
 	ConfigConnAllow bool       //is allow connected by config file
 	MaxTunnelNum    int
+	Version         string
 	sync.RWMutex
 }
 

+ 2 - 2
lib/goroutine/pool.go

@@ -1,8 +1,8 @@
 package goroutine
 
 import (
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/file"
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/file"
 	"github.com/panjf2000/ants/v2"
 	"io"
 	"net"

+ 119 - 47
lib/install/install.go

@@ -1,88 +1,151 @@
 package install
 
 import (
+	"ehang.io/nps/lib/common"
+	"encoding/json"
 	"errors"
 	"fmt"
+	"github.com/c4milo/unpackit"
 	"io"
 	"io/ioutil"
 	"log"
+	"net/http"
 	"os"
 	"path/filepath"
+	"runtime"
 	"strings"
-
-	"github.com/cnlh/nps/lib/common"
 )
 
-func InstallNps() {
-	unit := `[Unit]
-Description=nps - convenient proxy server
-Documentation=https://github.com/cnlh/nps/
-After=network-online.target remote-fs.target nss-lookup.target
-Wants=network-online.target`
-	service := `[Service]
-Type=simple
-KillMode=process
-Restart=always
-RestartSec=15s
-StandardOutput=append:/var/log/nps/nps.log
-ExecStartPre=/bin/echo 'Starting nps'
-ExecStopPost=/bin/echo 'Stopping nps'
-ExecStart=`
-	install := `[Install]
-WantedBy=multi-user.target`
+func UpdateNps() {
+	destPath := downloadLatest("server")
+	//复制文件到对应目录
+	copyStaticFile(destPath, "nps")
+	fmt.Println("Update completed, please restart")
+}
 
-	path := common.GetInstallPath()
-	if common.FileExists(path) {
-		log.Fatalf("the path %s has exist, does not support install", path)
-	}
-	MkidrDirAll(path, "conf", "web/static", "web/views")
+func UpdateNpc() {
+	destPath := downloadLatest("client")
 	//复制文件到对应目录
-	if err := CopyDir(filepath.Join(common.GetAppPath(), "web", "views"), filepath.Join(path, "web", "views")); err != nil {
-		log.Fatalln(err)
+	copyStaticFile(destPath, "npc")
+	fmt.Println("Update completed, please restart")
+}
+
+type release struct {
+	TagName string `json:"tag_name"`
+}
+
+func downloadLatest(bin string) string {
+	// get version
+	data, err := http.Get("https://api.github.com/repos/cnlh/nps/releases/latest")
+	if err != nil {
+		log.Fatal(err.Error())
+	}
+	b, err := ioutil.ReadAll(data.Body)
+	if err != nil {
+		log.Fatal(err)
+	}
+	rl := new(release)
+	json.Unmarshal(b, &rl)
+	version := rl.TagName
+	fmt.Println("the latest version is", version)
+	filename := runtime.GOOS + "_" + runtime.GOARCH + "_" + bin + ".tar.gz"
+	// download latest package
+	downloadUrl := fmt.Sprintf("https://ehang.io/nps/releases/download/%s/%s", version, filename)
+	fmt.Println("download package from ", downloadUrl)
+	resp, err := http.Get(downloadUrl)
+	if err != nil {
+		log.Fatal(err.Error())
 	}
-	if err := CopyDir(filepath.Join(common.GetAppPath(), "web", "static"), filepath.Join(path, "web", "static")); err != nil {
-		log.Fatalln(err)
+	destPath, err := unpackit.Unpack(resp.Body, "")
+	if err != nil {
+		log.Fatal(err)
 	}
-	if err := CopyDir(filepath.Join(common.GetAppPath(), "conf"), filepath.Join(path, "conf")); err != nil {
-		log.Fatalln(err)
+	if bin == "server" {
+		destPath = strings.Replace(destPath, "/web", "", -1)
+		destPath = strings.Replace(destPath, `\web`, "", -1)
+		destPath = strings.Replace(destPath, "/views", "", -1)
+		destPath = strings.Replace(destPath, `\views`, "", -1)
+	} else {
+		destPath = strings.Replace(destPath, `\conf`, "", -1)
+		destPath = strings.Replace(destPath, "/conf", "", -1)
 	}
+	return destPath
+}
 
+func copyStaticFile(srcPath, bin string) string {
+	path := common.GetInstallPath()
+	if bin == "nps" {
+		//复制文件到对应目录
+		if err := CopyDir(filepath.Join(srcPath, "web", "views"), filepath.Join(path, "web", "views")); err != nil {
+			log.Fatalln(err)
+		}
+		chMod(filepath.Join(path, "web", "views"), 0766)
+		if err := CopyDir(filepath.Join(srcPath, "web", "static"), filepath.Join(path, "web", "static")); err != nil {
+			log.Fatalln(err)
+		}
+		chMod(filepath.Join(path, "web", "static"), 0766)
+	}
+	binPath, _ := filepath.Abs(os.Args[0])
 	if !common.IsWindows() {
-		if _, err := copyFile(filepath.Join(common.GetAppPath(), "nps"), "/usr/bin/nps"); err != nil {
-			if _, err := copyFile(filepath.Join(common.GetAppPath(), "nps"), "/usr/local/bin/nps"); err != nil {
+		if _, err := copyFile(filepath.Join(srcPath, bin), "/usr/bin/"+bin); err != nil {
+			if _, err := copyFile(filepath.Join(srcPath, bin), "/usr/local/bin/"+bin); err != nil {
 				log.Fatalln(err)
 			} else {
-				os.Chmod("/usr/local/bin/nps", 0755)
-				service += "/usr/local/bin/nps"
-				log.Println("Executable files have been copied to", "/usr/local/bin/nps")
+				copyFile(filepath.Join(srcPath, bin), "/usr/local/bin/"+bin+"-update")
+				chMod("/usr/local/bin/"+bin+"-update", 0755)
+				binPath = "/usr/local/bin/" + bin
 			}
 		} else {
-			os.Chmod("/usr/bin/nps", 0755)
-			service += "/usr/bin/nps"
-			log.Println("Executable files have been copied to", "/usr/bin/nps")
+			copyFile(filepath.Join(srcPath, bin), "/usr/bin/"+bin+"-update")
+			chMod("/usr/bin/"+bin+"-update", 0755)
+			binPath = "/usr/bin/" + bin
 		}
-		systemd := unit + "\n\n" + service + "\n\n" + install
-		_ = os.Remove("/usr/lib/systemd/system/nps.service")
-		err := ioutil.WriteFile("/usr/lib/systemd/system/nps.service", []byte(systemd), 0644)
+	} else {
+		copyFile(filepath.Join(srcPath, bin+".exe"), filepath.Join(common.GetAppPath(), bin+"-update.exe"))
+		copyFile(filepath.Join(srcPath, bin+".exe"), filepath.Join(common.GetAppPath(), bin+".exe"))
+	}
+	chMod(binPath, 0755)
+	return binPath
+}
+
+func InstallNpc() {
+	path := common.GetInstallPath()
+	if !common.FileExists(path) {
+		err := os.Mkdir(path, 0755)
 		if err != nil {
-			log.Println("Write systemd service err ", err)
+			log.Fatal(err)
 		}
-		_ = os.Mkdir("/var/log/nps", 644)
 	}
+	copyStaticFile(common.GetAppPath(), "npc")
+}
+
+func InstallNps() string {
+	path := common.GetInstallPath()
+	if common.FileExists(path) {
+		MkidrDirAll(path, "web/static", "web/views")
+	} else {
+		MkidrDirAll(path, "conf", "web/static", "web/views")
+		// not copy config if the config file is exist
+		if err := CopyDir(filepath.Join(common.GetAppPath(), "conf"), filepath.Join(path, "conf")); err != nil {
+			log.Fatalln(err)
+		}
+		chMod(filepath.Join(path, "conf"), 0766)
+	}
+	binPath := copyStaticFile(common.GetAppPath(), "nps")
 	log.Println("install ok!")
 	log.Println("Static files and configuration files in the current directory will be useless")
 	log.Println("The new configuration file is located in", path, "you can edit them")
 	if !common.IsWindows() {
 		log.Println(`You can start with:
-sudo systemctl enable|disable|start|stop|restart|status nps
-or:
-nps test|start|stop|restart|status 
+nps start|stop|restart|uninstall|update or nps-update update
 anywhere!`)
 	} else {
 		log.Println(`You can copy executable files to any directory and start working with:
-nps.exe test|start|stop|restart|status
+nps.exe start|stop|restart|uninstall|update or nps-update.exe update
 now!`)
 	}
+	chMod(common.GetLogPath(), 0777)
+	return binPath
 }
 func MkidrDirAll(path string, v ...string) {
 	for _, item := range v {
@@ -119,6 +182,9 @@ func CopyDir(srcPath string, destPath string) error {
 			destNewPath := strings.Replace(path, srcPath, destPath, -1)
 			log.Println("copy file ::" + path + " to " + destNewPath)
 			copyFile(path, destNewPath)
+			if !common.IsWindows() {
+				chMod(destNewPath, 0766)
+			}
 		}
 		return nil
 	})
@@ -171,3 +237,9 @@ func pathExists(path string) (bool, error) {
 	}
 	return false, err
 }
+
+func chMod(name string, mode os.FileMode) {
+	if !common.IsWindows() {
+		os.Chmod(name, mode)
+	}
+}

+ 271 - 184
lib/mux/conn.go

@@ -1,7 +1,9 @@
 package mux
 
 import (
+	"ehang.io/nps/lib/common"
 	"errors"
+	"github.com/astaxie/beego/logs"
 	"io"
 	"math"
 	"net"
@@ -9,8 +11,6 @@ import (
 	"sync"
 	"sync/atomic"
 	"time"
-
-	"github.com/cnlh/nps/lib/common"
 )
 
 type conn struct {
@@ -145,25 +145,44 @@ func (s *conn) SetWriteDeadline(t time.Time) error {
 }
 
 type window struct {
-	remainingWait uint64 // 64bit alignment
-	off           uint32
-	maxSize       uint32
-	closeOp       bool
-	closeOpCh     chan struct{}
-	mux           *Mux
-}
-
-func (Self *window) unpack(ptrs uint64) (remaining, wait uint32) {
-	const mask = 1<<dequeueBits - 1
-	remaining = uint32((ptrs >> dequeueBits) & mask)
-	wait = uint32(ptrs & mask)
+	maxSizeDone uint64
+	// 64bit alignment
+	// maxSizeDone contains 4 parts
+	//   1       31       1      31
+	// wait   maxSize  useless  done
+	// wait zero means false, one means true
+	off       uint32
+	closeOp   bool
+	closeOpCh chan struct{}
+	mux       *Mux
+}
+
+const windowBits = 31
+const waitBits = dequeueBits + windowBits
+const mask1 = 1
+const mask31 = 1<<windowBits - 1
+
+func (Self *window) unpack(ptrs uint64) (maxSize, done uint32, wait bool) {
+	maxSize = uint32((ptrs >> dequeueBits) & mask31)
+	done = uint32(ptrs & mask31)
+	//logs.Warn("unpack", maxSize, done)
+	if ((ptrs >> waitBits) & mask1) == 1 {
+		wait = true
+		return
+	}
 	return
 }
 
-func (Self *window) pack(remaining, wait uint32) uint64 {
-	const mask = 1<<dequeueBits - 1
-	return (uint64(remaining) << dequeueBits) |
-		uint64(wait&mask)
+func (Self *window) pack(maxSize, done uint32, wait bool) uint64 {
+	//logs.Warn("pack", maxSize, done, wait)
+	if wait {
+		return (uint64(1)<<waitBits |
+			uint64(maxSize&mask31)<<dequeueBits) |
+			uint64(done&mask31)
+	}
+	return (uint64(0)<<waitBits |
+		uint64(maxSize&mask31)<<dequeueBits) |
+		uint64(done&mask31)
 }
 
 func (Self *window) New() {
@@ -180,24 +199,28 @@ func (Self *window) CloseWindow() {
 
 type ReceiveWindow struct {
 	window
-	bufQueue ReceiveWindowQueue
+	bufQueue *ReceiveWindowQueue
 	element  *common.ListElement
 	count    int8
+	bw       *writeBandwidth
 	once     sync.Once
+	// receive window send the current max size and read size to send window
+	// means done size actually store the size receive window has read
 }
 
 func (Self *ReceiveWindow) New(mux *Mux) {
 	// initial a window for receive
-	Self.bufQueue.New()
+	Self.bufQueue = NewReceiveWindowQueue()
 	Self.element = common.ListElementPool.Get()
-	Self.maxSize = common.MAXIMUM_SEGMENT_SIZE * 10
+	Self.maxSizeDone = Self.pack(common.MAXIMUM_SEGMENT_SIZE*30, 0, false)
 	Self.mux = mux
 	Self.window.New()
+	Self.bw = NewWriteBandwidth()
 }
 
-func (Self *ReceiveWindow) remainingSize(delta uint16) (n uint32) {
+func (Self *ReceiveWindow) remainingSize(maxSize uint32, delta uint16) (n uint32) {
 	// receive window remaining
-	l := int64(atomic.LoadUint32(&Self.maxSize)) - int64(Self.bufQueue.Len())
+	l := int64(maxSize) - int64(Self.bufQueue.Len())
 	l -= int64(delta)
 	if l > 0 {
 		n = uint32(l)
@@ -209,29 +232,46 @@ func (Self *ReceiveWindow) calcSize() {
 	// calculating maximum receive window size
 	if Self.count == 0 {
 		//logs.Warn("ping, bw", Self.mux.latency, Self.bw.Get())
-		conns := Self.mux.connMap.Size()
-		n := uint32(math.Float64frombits(atomic.LoadUint64(&Self.mux.latency)) *
-			Self.mux.bw.Get() / float64(conns))
-		if n < common.MAXIMUM_SEGMENT_SIZE*10 {
-			n = common.MAXIMUM_SEGMENT_SIZE * 10
+		//conns := Self.mux.connMap.Size()
+		muxBw := Self.mux.bw.Get()
+		connBw := Self.bw.Get()
+		//logs.Warn("muxbw connbw", muxBw, connBw)
+		var n uint32
+		if connBw > 0 && muxBw > 0 {
+			n = uint32(math.Float64frombits(atomic.LoadUint64(&Self.mux.latency)) *
+				(muxBw + connBw))
 		}
-		bufLen := Self.bufQueue.Len()
-		if n < bufLen {
-			n = bufLen
+		//logs.Warn(n)
+		if n < common.MAXIMUM_SEGMENT_SIZE*30 {
+			//logs.Warn("window small", n, Self.mux.bw.Get(), Self.bw.Get())
+			n = common.MAXIMUM_SEGMENT_SIZE * 30
 		}
-		if n < Self.maxSize/2 {
-			n = Self.maxSize / 2
+		for {
+			ptrs := atomic.LoadUint64(&Self.maxSizeDone)
+			size, read, wait := Self.unpack(ptrs)
+			if n < size/2 {
+				n = size / 2
+				// half reduce
+			}
+			// set the minimal size
+			if n > 2*size {
+				n = 2 * size
+				// twice grow
+			}
+			if connBw > 0 && muxBw > 0 {
+				limit := uint32(common.MAXIMUM_WINDOW_SIZE * (connBw / (muxBw + connBw)))
+				if n > limit {
+					logs.Warn("window too large, calculated:", n, "limit:", limit, connBw, muxBw)
+					n = limit
+				}
+			}
+			// set the maximum size
+			//logs.Warn("n", n)
+			if atomic.CompareAndSwapUint64(&Self.maxSizeDone, ptrs, Self.pack(n, read, wait)) {
+				// only change the maxSize
+				break
+			}
 		}
-		// set the minimal size
-		if n > 2*Self.maxSize {
-			n = 2 * Self.maxSize
-		}
-		if n > (common.MAXIMUM_WINDOW_SIZE / uint32(conns)) {
-			n = common.MAXIMUM_WINDOW_SIZE / uint32(conns)
-		}
-		// set the maximum size
-		//logs.Warn("n", n)
-		atomic.StoreUint32(&Self.maxSize, n)
 		Self.count = -10
 	}
 	Self.count += 1
@@ -243,30 +283,40 @@ func (Self *ReceiveWindow) Write(buf []byte, l uint16, part bool, id int32) (err
 		return errors.New("conn.receiveWindow: write on closed window")
 	}
 	element, err := NewListElement(buf, l, part)
-	//logs.Warn("push the buf", len(buf), l, (&element).l)
+	//logs.Warn("push the buf", len(buf), l, element.L)
 	if err != nil {
 		return
 	}
 	Self.calcSize() // calculate the max window size
-	var wait uint32
+	var wait bool
+	var maxSize, read uint32
 start:
-	ptrs := atomic.LoadUint64(&Self.remainingWait)
-	_, wait = Self.unpack(ptrs)
-	newRemaining := Self.remainingSize(l)
+	ptrs := atomic.LoadUint64(&Self.maxSizeDone)
+	maxSize, read, wait = Self.unpack(ptrs)
+	remain := Self.remainingSize(maxSize, l)
 	// calculate the remaining window size now, plus the element we will push
-	if newRemaining == 0 {
+	if remain == 0 && !wait {
 		//logs.Warn("window full true", remaining)
-		wait = 1
-	}
-	if !atomic.CompareAndSwapUint64(&Self.remainingWait, ptrs, Self.pack(0, wait)) {
-		goto start
-		// another goroutine change the status, make sure shall we need wait
-	}
+		wait = true
+		if !atomic.CompareAndSwapUint64(&Self.maxSizeDone, ptrs, Self.pack(maxSize, read, wait)) {
+			// only change the wait status, not send the read size
+			goto start
+			// another goroutine change the status, make sure shall we need wait
+		}
+		//logs.Warn("receive window full")
+	} else if !wait {
+		if !atomic.CompareAndSwapUint64(&Self.maxSizeDone, ptrs, Self.pack(maxSize, 0, wait)) {
+			// reset read size here, and send the read size directly
+			goto start
+			// another goroutine change the status, make sure shall we need wait
+		}
+	} // maybe there are still some data received even if window is full, just keep the wait status
+	// and push into queue. when receive window read enough, send window will be acknowledged.
 	Self.bufQueue.Push(element)
 	// status check finish, now we can push the element into the queue
-	if wait == 0 {
-		Self.mux.sendInfo(common.MUX_MSG_SEND_OK, id, Self.maxSize, newRemaining)
-		// send the remaining window size, not including zero size
+	if !wait {
+		Self.mux.sendInfo(common.MUX_MSG_SEND_OK, id, Self.pack(maxSize, read, false))
+		// send the current status to send window
 	}
 	return nil
 }
@@ -275,9 +325,16 @@ func (Self *ReceiveWindow) Read(p []byte, id int32) (n int, err error) {
 	if Self.closeOp {
 		return 0, io.EOF // receive close signal, returns eof
 	}
+	Self.bw.StartRead()
+	n, err = Self.readFromQueue(p, id)
+	Self.bw.SetCopySize(uint16(n))
+	return
+}
+
+func (Self *ReceiveWindow) readFromQueue(p []byte, id int32) (n int, err error) {
 	pOff := 0
 	l := 0
-	//logs.Warn("receive window read off, element.l", Self.off, Self.element.l)
+	//logs.Warn("receive window read off, element.l", Self.off, Self.element.L)
 copyData:
 	if Self.off == uint32(Self.element.L) {
 		// on the first Read method invoked, Self.off and Self.element.l
@@ -289,13 +346,13 @@ copyData:
 		Self.element, err = Self.bufQueue.Pop()
 		// if the queue is empty, Pop method will wait until one element push
 		// into the queue successful, or timeout.
-		// timer start on timeout parameter is set up ,
-		// reset to 60s if timeout and data still available
+		// timer start on timeout parameter is set up
 		Self.off = 0
 		if err != nil {
-			return // queue receive stop or time out, break the loop and return
+			Self.CloseWindow() // also close the window, to avoid read twice
+			return             // queue receive stop or time out, break the loop and return
 		}
-		//logs.Warn("pop element", Self.element.l, Self.element.part)
+		//logs.Warn("pop element", Self.element.L, Self.element.Part)
 	}
 	l = copy(p[pOff:], Self.element.Buf[Self.off:Self.element.L])
 	pOff += l
@@ -317,22 +374,41 @@ copyData:
 }
 
 func (Self *ReceiveWindow) sendStatus(id int32, l uint16) {
-	var remaining, wait uint32
+	var maxSize, read uint32
+	var wait bool
 	for {
-		ptrs := atomic.LoadUint64(&Self.remainingWait)
-		remaining, wait = Self.unpack(ptrs)
-		remaining += uint32(l)
-		if atomic.CompareAndSwapUint64(&Self.remainingWait, ptrs, Self.pack(remaining, 0)) {
-			break
+		ptrs := atomic.LoadUint64(&Self.maxSizeDone)
+		maxSize, read, wait = Self.unpack(ptrs)
+		if read <= (read+uint32(l))&mask31 {
+			read += uint32(l)
+			remain := Self.remainingSize(maxSize, 0)
+			if wait && remain > 0 || read >= maxSize/2 || remain == maxSize {
+				if atomic.CompareAndSwapUint64(&Self.maxSizeDone, ptrs, Self.pack(maxSize, 0, false)) {
+					// now we get the current window status success
+					// receive window free up some space we need acknowledge send window, also reset the read size
+					// still having a condition that receive window is empty and not send the status to send window
+					// so send the status here
+					//logs.Warn("receive window free up some space", remain)
+					Self.mux.sendInfo(common.MUX_MSG_SEND_OK, id, Self.pack(maxSize, read, false))
+					break
+				}
+			} else {
+				if atomic.CompareAndSwapUint64(&Self.maxSizeDone, ptrs, Self.pack(maxSize, read, wait)) {
+					// receive window not into the wait status, or still not having any space now,
+					// just change the read size
+					break
+				}
+			}
+		} else {
+			//overflow
+			if atomic.CompareAndSwapUint64(&Self.maxSizeDone, ptrs, Self.pack(maxSize, uint32(l), wait)) {
+				// reset to l
+				Self.mux.sendInfo(common.MUX_MSG_SEND_OK, id, Self.pack(maxSize, read, false))
+				break
+			}
 		}
 		runtime.Gosched()
 		// another goroutine change remaining or wait status, make sure
-		// we need acknowledge other side
-	}
-	// now we get the current window status success
-	if wait == 1 {
-		//logs.Warn("send the wait status", remaining)
-		Self.mux.sendInfo(common.MUX_MSG_SEND_OK, id, atomic.LoadUint32(&Self.maxSize), remaining)
 	}
 	return
 }
@@ -361,14 +437,14 @@ func (Self *ReceiveWindow) release() {
 	//	common.ListElementPool.Put(Self.element)
 	//}
 	for {
-		Self.element = Self.bufQueue.TryPop()
-		if Self.element == nil {
+		ele := Self.bufQueue.TryPop()
+		if ele == nil {
 			return
 		}
-		if Self.element.Buf != nil {
-			common.WindowBuff.Put(Self.element.Buf)
+		if ele.Buf != nil {
+			common.WindowBuff.Put(ele.Buf)
 		}
-		common.ListElementPool.Put(Self.element)
+		common.ListElementPool.Put(ele)
 	} // release resource
 }
 
@@ -377,12 +453,14 @@ type SendWindow struct {
 	buf       []byte
 	setSizeCh chan struct{}
 	timeout   time.Time
+	// send window receive the receive window max size and read size
+	// done size store the size send window has send, send and read will be totally equal
+	// so send minus read, send window can get the current window size remaining
 }
 
 func (Self *SendWindow) New(mux *Mux) {
 	Self.setSizeCh = make(chan struct{})
-	Self.maxSize = common.MAXIMUM_SEGMENT_SIZE * 10
-	atomic.AddUint64(&Self.remainingWait, uint64(common.MAXIMUM_SEGMENT_SIZE*10)<<dequeueBits)
+	Self.maxSizeDone = Self.pack(common.MAXIMUM_SEGMENT_SIZE*30, 0, false)
 	Self.mux = mux
 	Self.window.New()
 }
@@ -393,7 +471,15 @@ func (Self *SendWindow) SetSendBuf(buf []byte) {
 	Self.off = 0
 }
 
-func (Self *SendWindow) SetSize(windowSize, newRemaining uint32) (closed bool) {
+func (Self *SendWindow) remainingSize(maxSize, send uint32) uint32 {
+	l := int64(maxSize&mask31) - int64(send&mask31)
+	if l > 0 {
+		return uint32(l)
+	}
+	return 0
+}
+
+func (Self *SendWindow) SetSize(currentMaxSizeDone uint64) (closed bool) {
 	// set the window size from receive window
 	defer func() {
 		if recover() != nil {
@@ -405,26 +491,34 @@ func (Self *SendWindow) SetSize(windowSize, newRemaining uint32) (closed bool) {
 		return true
 	}
 	//logs.Warn("set send window size to ", windowSize, newRemaining)
-	var remaining, wait, newWait uint32
+	var maxsize, send uint32
+	var wait, newWait bool
+	currentMaxSize, read, _ := Self.unpack(currentMaxSizeDone)
 	for {
-		ptrs := atomic.LoadUint64(&Self.remainingWait)
-		remaining, wait = Self.unpack(ptrs)
-		if remaining == newRemaining {
-			//logs.Warn("waiting for another window size")
-			return false // waiting for receive another usable window size
+		ptrs := atomic.LoadUint64(&Self.maxSizeDone)
+		maxsize, send, wait = Self.unpack(ptrs)
+		if read > send {
+			logs.Error("window read > send: max size:", currentMaxSize, "read:", read, "send", send)
+			return
+		}
+		if read == 0 && currentMaxSize == maxsize {
+			return
 		}
-		if newRemaining == 0 && wait == 1 {
-			newWait = 1 // keep the wait status,
-			// also if newRemaining is not zero, change wait to 0
+		send -= read
+		remain := Self.remainingSize(currentMaxSize, send)
+		if remain == 0 && wait {
+			// just keep the wait status
+			newWait = true
 		}
-		if atomic.CompareAndSwapUint64(&Self.remainingWait, ptrs, Self.pack(newRemaining, newWait)) {
+		// remain > 0, change wait to false. or remain == 0, wait is false, just keep it
+		if atomic.CompareAndSwapUint64(&Self.maxSizeDone, ptrs, Self.pack(currentMaxSize, send, newWait)) {
 			break
 		}
 		// anther goroutine change wait status or window size
 	}
-	if wait == 1 {
+	if wait && !newWait {
 		// send window into the wait status, need notice the channel
-		//logs.Warn("send window remaining size is 0")
+		//logs.Warn("send window allow")
 		Self.allow()
 	}
 	// send window not into the wait status, so just do slide
@@ -443,7 +537,22 @@ func (Self *SendWindow) allow() {
 }
 
 func (Self *SendWindow) sent(sentSize uint32) {
-	atomic.AddUint64(&Self.remainingWait, ^(uint64(sentSize)<<dequeueBits - 1))
+	var maxSie, send uint32
+	var wait bool
+	for {
+		ptrs := atomic.LoadUint64(&Self.maxSizeDone)
+		maxSie, send, wait = Self.unpack(ptrs)
+		if (send+sentSize)&mask31 < send {
+			// overflow
+			runtime.Gosched()
+			continue
+		}
+		if atomic.CompareAndSwapUint64(&Self.maxSizeDone, ptrs, Self.pack(maxSie, send+sentSize, wait)) {
+			// set the send size
+			//logs.Warn("sent", maxSie, send+sentSize, wait)
+			break
+		}
+	}
 }
 
 func (Self *SendWindow) WriteTo() (p []byte, sendSize uint32, part bool, err error) {
@@ -456,12 +565,14 @@ func (Self *SendWindow) WriteTo() (p []byte, sendSize uint32, part bool, err err
 		return nil, 0, false, io.EOF
 		// send window buff is drain, return eof and get another one
 	}
-	var remaining uint32
+	var maxSize, send uint32
 start:
-	ptrs := atomic.LoadUint64(&Self.remainingWait)
-	remaining, _ = Self.unpack(ptrs)
-	if remaining == 0 {
-		if !atomic.CompareAndSwapUint64(&Self.remainingWait, ptrs, Self.pack(0, 1)) {
+	ptrs := atomic.LoadUint64(&Self.maxSizeDone)
+	maxSize, send, _ = Self.unpack(ptrs)
+	remain := Self.remainingSize(maxSize, send)
+	if remain == 0 {
+		if !atomic.CompareAndSwapUint64(&Self.maxSizeDone, ptrs, Self.pack(maxSize, send, true)) {
+			// just change the status wait status
 			goto start // another goroutine change the window, try again
 		}
 		// into the wait status
@@ -474,17 +585,17 @@ start:
 		goto start
 	}
 	// there are still remaining window
-	//logs.Warn("rem", remaining)
+	//logs.Warn("rem", remain, maxSize, send)
 	if len(Self.buf[Self.off:]) > common.MAXIMUM_SEGMENT_SIZE {
 		sendSize = common.MAXIMUM_SEGMENT_SIZE
 		//logs.Warn("cut buf by mss")
 	} else {
 		sendSize = uint32(len(Self.buf[Self.off:]))
 	}
-	if remaining < sendSize {
+	if remain < sendSize {
 		// usable window size is small than
 		// window MAXIMUM_SEGMENT_SIZE or send buf left
-		sendSize = remaining
+		sendSize = remain
 		//logs.Warn("cut buf by remainingsize", sendSize, len(Self.buf[Self.off:]))
 	}
 	//logs.Warn("send size", sendSize)
@@ -499,8 +610,16 @@ start:
 
 func (Self *SendWindow) waitReceiveWindow() (err error) {
 	t := Self.timeout.Sub(time.Now())
-	if t < 0 {
-		t = time.Minute * 5
+	if t < 0 { // not set the timeout, wait for it as long as connection close
+		select {
+		case _, ok := <-Self.setSizeCh:
+			if !ok {
+				return errors.New("conn.writeWindow: window closed")
+			}
+			return nil
+		case <-Self.closeOpCh:
+			return errors.New("conn.writeWindow: window closed")
+		}
 	}
 	timer := time.NewTimer(t)
 	defer timer.Stop()
@@ -555,81 +674,49 @@ func (Self *SendWindow) SetTimeOut(t time.Time) {
 	Self.timeout = t
 }
 
-//type bandwidth struct {
-//	readStart     time.Time
-//	lastReadStart time.Time
-//	readEnd       time.Time
-//	lastReadEnd time.Time
-//	bufLength     int
-//	lastBufLength int
-//	count         int8
-//	readBW        float64
-//	writeBW       float64
-//	readBandwidth float64
-//}
-//
-//func (Self *bandwidth) StartRead() {
-//	Self.lastReadStart, Self.readStart = Self.readStart, time.Now()
-//	if !Self.lastReadStart.IsZero() {
-//		if Self.count == -5 {
-//			Self.calcBandWidth()
-//		}
-//	}
-//}
-//
-//func (Self *bandwidth) EndRead() {
-//	Self.lastReadEnd, Self.readEnd = Self.readEnd, time.Now()
-//	if Self.count == -5 {
-//		Self.calcWriteBandwidth()
-//	}
-//	if Self.count == 0 {
-//		Self.calcReadBandwidth()
-//		Self.count = -6
-//	}
-//	Self.count += 1
-//}
-//
-//func (Self *bandwidth) SetCopySize(n int) {
-//	// must be invoke between StartRead and EndRead
-//	Self.lastBufLength, Self.bufLength = Self.bufLength, n
-//}
-//// calculating
-//// start end start end
-////     read     read
-////        write
-//
-//func (Self *bandwidth) calcBandWidth()  {
-//	t := Self.readStart.Sub(Self.lastReadStart)
-//	if Self.lastBufLength >= 32768 {
-//		Self.readBandwidth = float64(Self.lastBufLength) / t.Seconds()
-//	}
-//}
-//
-//func (Self *bandwidth) calcReadBandwidth() {
-//	// Bandwidth between nps and npc
-//	readTime := Self.readEnd.Sub(Self.readStart)
-//	Self.readBW = float64(Self.bufLength) / readTime.Seconds()
-//	//logs.Warn("calc read bw", Self.readBW, Self.bufLength, readTime.Seconds())
-//}
-//
-//func (Self *bandwidth) calcWriteBandwidth() {
-//	// Bandwidth between nps and user, npc and application
-//	writeTime := Self.readStart.Sub(Self.lastReadEnd)
-//	Self.writeBW = float64(Self.lastBufLength) / writeTime.Seconds()
-//	//logs.Warn("calc write bw", Self.writeBW, Self.bufLength, writeTime.Seconds())
-//}
-//
-//func (Self *bandwidth) Get() (bw float64) {
-//	// The zero value, 0 for numeric types
-//	if Self.writeBW == 0 && Self.readBW == 0 {
-//		//logs.Warn("bw both 0")
-//		return 100
-//	}
-//	if Self.writeBW == 0 && Self.readBW != 0 {
-//		return Self.readBW
-//	}
-//	if Self.readBW == 0 && Self.writeBW != 0 {
-//		return Self.writeBW
-//	}
-//	return Self.readBandwidth
-//}
+type writeBandwidth struct {
+	writeBW   uint64 // store in bits, but it's float64
+	readEnd   time.Time
+	duration  float64
+	bufLength uint32
+}
+
+const writeCalcThreshold uint32 = 5 * 1024 * 1024
+
+func NewWriteBandwidth() *writeBandwidth {
+	return &writeBandwidth{}
+}
+
+func (Self *writeBandwidth) StartRead() {
+	if Self.readEnd.IsZero() {
+		Self.readEnd = time.Now()
+	}
+	Self.duration += time.Now().Sub(Self.readEnd).Seconds()
+	if Self.bufLength >= writeCalcThreshold {
+		Self.calcBandWidth()
+	}
+}
+
+func (Self *writeBandwidth) SetCopySize(n uint16) {
+	Self.bufLength += uint32(n)
+	Self.endRead()
+}
+
+func (Self *writeBandwidth) endRead() {
+	Self.readEnd = time.Now()
+}
+
+func (Self *writeBandwidth) calcBandWidth() {
+	atomic.StoreUint64(&Self.writeBW, math.Float64bits(float64(Self.bufLength)/Self.duration))
+	Self.bufLength = 0
+	Self.duration = 0
+}
+
+func (Self *writeBandwidth) Get() (bw float64) {
+	// The zero value, 0 for numeric types
+	bw = math.Float64frombits(atomic.LoadUint64(&Self.writeBW))
+	if bw <= 0 {
+		bw = 0
+	}
+	return
+}

+ 35 - 8
lib/mux/mux.go

@@ -5,11 +5,12 @@ import (
 	"io"
 	"math"
 	"net"
+	"os"
 	"sync/atomic"
 	"time"
 
+	"ehang.io/nps/lib/common"
 	"github.com/astaxie/beego/logs"
-	"github.com/cnlh/nps/lib/common"
 )
 
 type Mux struct {
@@ -34,13 +35,18 @@ type Mux struct {
 func NewMux(c net.Conn, connType string) *Mux {
 	//c.(*net.TCPConn).SetReadBuffer(0)
 	//c.(*net.TCPConn).SetWriteBuffer(0)
+	_ = c.SetDeadline(time.Time{})
+	fd, err := getConnFd(c)
+	if err != nil {
+		logs.Warn(err)
+	}
 	m := &Mux{
 		conn:      c,
 		connMap:   NewConnMap(),
 		id:        0,
 		closeChan: make(chan struct{}, 1),
 		newConnCh: make(chan *conn),
-		bw:        new(bandwidth),
+		bw:        NewBandwidth(fd),
 		IsClose:   false,
 		connType:  connType,
 		pingCh:    make(chan []byte),
@@ -173,9 +179,9 @@ func (s *Mux) ping() {
 		s.sendInfo(common.MUX_PING_FLAG, common.MUX_PING, now)
 		// send the ping flag and get the latency first
 		ticker := time.NewTicker(time.Second * 5)
+		defer ticker.Stop()
 		for {
 			if s.IsClose {
-				ticker.Stop()
 				break
 			}
 			select {
@@ -198,6 +204,7 @@ func (s *Mux) ping() {
 			}
 			atomic.AddUint32(&s.pingOk, 1)
 		}
+		return
 	}()
 }
 
@@ -213,7 +220,7 @@ func (s *Mux) pingReturn() {
 			case data = <-s.pingCh:
 				atomic.StoreUint32(&s.pingCheckTime, 0)
 			case <-s.closeChan:
-				break
+				return
 			}
 			_ = now.UnmarshalText(data)
 			latency := time.Now().UTC().Sub(now).Seconds() / 2
@@ -296,7 +303,7 @@ func (s *Mux) readSession() {
 					if connection.isClose {
 						continue
 					}
-					connection.sendWindow.SetSize(pack.Window, pack.ReadLength)
+					connection.sendWindow.SetSize(pack.Window)
 					continue
 				case common.MUX_CONN_CLOSE: //close the connection
 					connection.closeFlag = true
@@ -390,13 +397,19 @@ type bandwidth struct {
 	readStart     time.Time
 	lastReadStart time.Time
 	bufLength     uint32
+	fd            *os.File
+	calcThreshold uint32
+}
+
+func NewBandwidth(fd *os.File) *bandwidth {
+	return &bandwidth{fd: fd}
 }
 
 func (Self *bandwidth) StartRead() {
 	if Self.readStart.IsZero() {
 		Self.readStart = time.Now()
 	}
-	if Self.bufLength >= common.MAXIMUM_SEGMENT_SIZE*300 {
+	if Self.bufLength >= Self.calcThreshold {
 		Self.lastReadStart, Self.readStart = Self.readStart, time.Now()
 		Self.calcBandWidth()
 	}
@@ -408,7 +421,21 @@ func (Self *bandwidth) SetCopySize(n uint16) {
 
 func (Self *bandwidth) calcBandWidth() {
 	t := Self.readStart.Sub(Self.lastReadStart)
-	atomic.StoreUint64(&Self.readBandwidth, math.Float64bits(float64(Self.bufLength)/t.Seconds()))
+	bufferSize, err := sysGetSock(Self.fd)
+	//logs.Warn(bufferSize)
+	if err != nil {
+		logs.Warn(err)
+		Self.bufLength = 0
+		return
+	}
+	if Self.bufLength >= uint32(bufferSize) {
+		atomic.StoreUint64(&Self.readBandwidth, math.Float64bits(float64(Self.bufLength)/t.Seconds()))
+		// calculate the whole socket buffer, the time meaning to fill the buffer
+		//logs.Warn(Self.Get())
+	} else {
+		Self.calcThreshold = uint32(bufferSize)
+	}
+	// socket buffer size is bigger than bufLength, so we don't calculate it
 	Self.bufLength = 0
 }
 
@@ -416,7 +443,7 @@ func (Self *bandwidth) Get() (bw float64) {
 	// The zero value, 0 for numeric types
 	bw = math.Float64frombits(atomic.LoadUint64(&Self.readBandwidth))
 	if bw <= 0 {
-		bw = 100
+		bw = 0
 	}
 	//logs.Warn(bw)
 	return

+ 8 - 2
lib/mux/mux_test.go

@@ -2,9 +2,9 @@ package mux
 
 import (
 	"bufio"
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/goroutine"
 	"fmt"
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/goroutine"
 	"io"
 	"log"
 	"net"
@@ -34,6 +34,7 @@ func TestNewMux(t *testing.T) {
 	time.Sleep(time.Second * 3)
 	go func() {
 		m2 := NewMux(conn2, "tcp")
+		//m2 := NewMux(conn2, "kcp")
 		for {
 			//logs.Warn("npc starting accept")
 			c, err := m2.Accept()
@@ -83,6 +84,7 @@ func TestNewMux(t *testing.T) {
 
 	go func() {
 		m1 := NewMux(conn1, "tcp")
+		//m1 := NewMux(conn1, "kcp")
 		l, err := net.Listen("tcp", "127.0.0.1:7777")
 		if err != nil {
 			logs.Warn(err)
@@ -145,11 +147,13 @@ func TestNewMux(t *testing.T) {
 func server() {
 	var err error
 	l, err := net.Listen("tcp", "127.0.0.1:9999")
+	//l, err := kcp.Listen("127.0.0.1:9999")
 	if err != nil {
 		logs.Warn(err)
 	}
 	go func() {
 		conn1, err = l.Accept()
+		//logs.Info("accept", conn1)
 		if err != nil {
 			logs.Warn(err)
 		}
@@ -160,6 +164,8 @@ func server() {
 func client() {
 	var err error
 	conn2, err = net.Dial("tcp", "127.0.0.1:9999")
+	//logs.Warn("dial")
+	//conn2, err = kcp.Dial("127.0.0.1:9999")
 	if err != nil {
 		logs.Warn(err)
 	}

+ 16 - 8
lib/mux/pconn.go

@@ -6,15 +6,17 @@ import (
 )
 
 type PortConn struct {
-	Conn  net.Conn
-	rs    []byte
-	start int
+	Conn     net.Conn
+	rs       []byte
+	readMore bool
+	start    int
 }
 
-func newPortConn(conn net.Conn, rs []byte) *PortConn {
+func newPortConn(conn net.Conn, rs []byte, readMore bool) *PortConn {
 	return &PortConn{
-		Conn: conn,
-		rs:   rs,
+		Conn:     conn,
+		rs:       rs,
+		readMore: readMore,
 	}
 }
 
@@ -29,9 +31,15 @@ func (pConn *PortConn) Read(b []byte) (n int, err error) {
 		defer func() {
 			pConn.start = len(pConn.rs)
 		}()
-		return copy(b, pConn.rs[pConn.start:]), nil
+		n = copy(b, pConn.rs[pConn.start:])
+		if !pConn.readMore {
+			return
+		}
 	}
-	return pConn.Conn.Read(b)
+	var n2 = 0
+	n2, err = pConn.Conn.Read(b[n:])
+	n = n + n2
+	return
 }
 
 func (pConn *PortConn) Write(b []byte) (n int, err error) {

+ 4 - 2
lib/mux/pmux.go

@@ -12,8 +12,8 @@ import (
 	"strings"
 	"time"
 
+	"ehang.io/nps/lib/common"
 	"github.com/astaxie/beego/logs"
-	"github.com/cnlh/nps/lib/common"
 	"github.com/pkg/errors"
 )
 
@@ -89,6 +89,7 @@ func (pMux *PortMux) process(conn net.Conn) {
 	var ch chan *PortConn
 	var rs []byte
 	var buffer bytes.Buffer
+	var readMore = false
 	switch common.BytesToNum(buf) {
 	case HTTP_CONNECT, HTTP_DELETE, HTTP_GET, HTTP_HEAD, HTTP_OPTIONS, HTTP_POST, HTTP_PUT, HTTP_TRACE: //http and manager
 		buffer.Reset()
@@ -123,6 +124,7 @@ func (pMux *PortMux) process(conn net.Conn) {
 	case CLIENT: // client connection
 		ch = pMux.clientConn
 	default: // https
+		readMore = true
 		ch = pMux.httpsConn
 	}
 	if len(rs) == 0 {
@@ -131,7 +133,7 @@ func (pMux *PortMux) process(conn net.Conn) {
 	timer := time.NewTimer(ACCEPT_TIME_OUT)
 	select {
 	case <-timer.C:
-	case ch <- newPortConn(conn, rs):
+	case ch <- newPortConn(conn, rs, readMore):
 	}
 }
 

+ 19 - 10
lib/mux/queue.go

@@ -1,8 +1,8 @@
 package mux
 
 import (
+	"ehang.io/nps/lib/common"
 	"errors"
-	"github.com/cnlh/nps/lib/common"
 	"io"
 	"math"
 	"runtime"
@@ -209,23 +209,26 @@ func NewListElement(buf []byte, l uint16, part bool) (element *common.ListElemen
 }
 
 type ReceiveWindowQueue struct {
+	lengthWait uint64
 	chain      *bufChain
 	stopOp     chan struct{}
 	readOp     chan struct{}
-	lengthWait uint64 // really strange ???? need put here
 	// https://golang.org/pkg/sync/atomic/#pkg-note-BUG
 	// On non-Linux ARM, the 64-bit functions use instructions unavailable before the ARMv6k core.
 	// On ARM, x86-32, and 32-bit MIPS, it is the caller's responsibility
 	// to arrange for 64-bit alignment of 64-bit words accessed atomically.
 	// The first word in a variable or in an allocated struct, array, or slice can be relied upon to be 64-bit aligned.
-	timeout    time.Time
+	timeout time.Time
 }
 
-func (Self *ReceiveWindowQueue) New() {
-	Self.readOp = make(chan struct{})
-	Self.chain = new(bufChain)
-	Self.chain.new(64)
-	Self.stopOp = make(chan struct{}, 2)
+func NewReceiveWindowQueue() *ReceiveWindowQueue {
+	queue := ReceiveWindowQueue{
+		chain:  new(bufChain),
+		stopOp: make(chan struct{}, 2),
+		readOp: make(chan struct{}),
+	}
+	queue.chain.new(64)
+	return &queue
 }
 
 func (Self *ReceiveWindowQueue) Push(element *common.ListElement) {
@@ -300,8 +303,14 @@ func (Self *ReceiveWindowQueue) waitPush() (err error) {
 	//logs.Warn("wait push")
 	//defer logs.Warn("wait push finish")
 	t := Self.timeout.Sub(time.Now())
-	if t <= 0 {
-		t = time.Minute * 5
+	if t <= 0 { // not set the timeout, so wait for it without timeout, just like a tcp connection
+		select {
+		case <-Self.readOp:
+			return nil
+		case <-Self.stopOp:
+			err = io.EOF
+			return
+		}
 	}
 	timer := time.NewTimer(t)
 	defer timer.Stop()

+ 46 - 0
lib/mux/sysGetsock_nowindows.go

@@ -0,0 +1,46 @@
+// +build !windows
+
+package mux
+
+import (
+	"errors"
+	"github.com/xtaci/kcp-go"
+	"net"
+	"os"
+	"syscall"
+)
+
+func sysGetSock(fd *os.File) (bufferSize int, err error) {
+	if fd != nil {
+		return syscall.GetsockoptInt(int(fd.Fd()), syscall.SOL_SOCKET, syscall.SO_RCVBUF)
+	} else {
+		return 5 * 1024 * 1024, nil
+	}
+}
+
+func getConnFd(c net.Conn) (fd *os.File, err error) {
+	switch c.(type) {
+	case *net.TCPConn:
+		fd, err = c.(*net.TCPConn).File()
+		if err != nil {
+			return
+		}
+		return
+	case *net.UDPConn:
+		fd, err = c.(*net.UDPConn).File()
+		if err != nil {
+			return
+		}
+		return
+	case *kcp.UDPSession:
+		//fd, err = (*net.UDPConn)(unsafe.Pointer(c.(*kcp.UDPSession))).File()
+		//if err != nil {
+		//	return
+		//}
+		// Todo
+		return
+	default:
+		err = errors.New("mux:unknown conn type, only tcp or kcp")
+		return
+	}
+}

+ 46 - 0
lib/mux/sysGetsock_windows.go

@@ -0,0 +1,46 @@
+// +build windows
+
+package mux
+
+import (
+	"errors"
+	"github.com/xtaci/kcp-go"
+	"net"
+	"os"
+)
+
+func sysGetSock(fd *os.File) (bufferSize int, err error) {
+	// https://github.com/golang/sys/blob/master/windows/syscall_windows.go#L1184
+	// not support, WTF???
+	// Todo
+	// return syscall.GetsockoptInt((syscall.Handle)(unsafe.Pointer(fd.Fd())), syscall.SOL_SOCKET, syscall.SO_RCVBUF)
+	bufferSize = 5 * 1024 * 1024
+	return
+}
+
+func getConnFd(c net.Conn) (fd *os.File, err error) {
+	switch c.(type) {
+	case *net.TCPConn:
+		//fd, err = c.(*net.TCPConn).File()
+		//if err != nil {
+		//	return
+		//}
+		return
+	case *net.UDPConn:
+		//fd, err = c.(*net.UDPConn).File()
+		//if err != nil {
+		//	return
+		//}
+		return
+	case *kcp.UDPSession:
+		//fd, err = (*net.UDPConn)(unsafe.Pointer(c.(*kcp.UDPSession))).File()
+		//if err != nil {
+		//	return
+		//}
+		// Todo
+		return
+	default:
+		err = errors.New("mux:unknown conn type, only tcp or kcp")
+		return
+	}
+}

+ 2 - 2
lib/version/version.go

@@ -1,8 +1,8 @@
 package version
 
-const VERSION = "0.24.0"
+const VERSION = "0.26.0"
 
 // Compulsory minimum version, Minimum downward compatibility to this version
 func GetVersion() string {
-	return "0.24.0"
+	return "0.26.0"
 }

+ 1 - 1
server/connection/connection.go

@@ -5,9 +5,9 @@ import (
 	"os"
 	"strconv"
 
+	"ehang.io/nps/lib/mux"
 	"github.com/astaxie/beego"
 	"github.com/astaxie/beego/logs"
-	"github.com/cnlh/nps/lib/mux"
 )
 
 var pMux *mux.PortMux

+ 4 - 4
server/proxy/base.go

@@ -6,11 +6,11 @@ import (
 	"net/http"
 	"sync"
 
+	"ehang.io/nps/bridge"
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/conn"
+	"ehang.io/nps/lib/file"
 	"github.com/astaxie/beego/logs"
-	"github.com/cnlh/nps/bridge"
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/conn"
-	"github.com/cnlh/nps/lib/file"
 )
 
 type Service interface {

+ 8 - 8
server/proxy/http.go

@@ -13,13 +13,13 @@ import (
 	"strings"
 	"sync"
 
+	"ehang.io/nps/bridge"
+	"ehang.io/nps/lib/cache"
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/conn"
+	"ehang.io/nps/lib/file"
+	"ehang.io/nps/server/connection"
 	"github.com/astaxie/beego/logs"
-	"github.com/cnlh/nps/bridge"
-	"github.com/cnlh/nps/lib/cache"
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/conn"
-	"github.com/cnlh/nps/lib/file"
-	"github.com/cnlh/nps/server/connection"
 )
 
 type httpServer struct {
@@ -171,11 +171,11 @@ reset:
 			}
 		}()
 		for {
-			if resp, err := http.ReadResponse(bufio.NewReader(connClient), r); err != nil {
+			if resp, err := http.ReadResponse(bufio.NewReader(connClient), r); err != nil || resp == nil {
 				return
 			} else {
 				//if the cache is start and the response is in the extension,store the response to the cache list
-				if s.useCache && strings.Contains(r.URL.Path, ".") {
+				if s.useCache && r.URL != nil && strings.Contains(r.URL.Path, ".") {
 					b, err := httputil.DumpResponse(resp, true)
 					if err != nil {
 						return

+ 5 - 5
server/proxy/https.go

@@ -6,13 +6,13 @@ import (
 	"net/url"
 	"sync"
 
+	"ehang.io/nps/lib/cache"
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/conn"
+	"ehang.io/nps/lib/crypt"
+	"ehang.io/nps/lib/file"
 	"github.com/astaxie/beego"
 	"github.com/astaxie/beego/logs"
-	"github.com/cnlh/nps/lib/cache"
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/conn"
-	"github.com/cnlh/nps/lib/crypt"
-	"github.com/cnlh/nps/lib/file"
 	"github.com/pkg/errors"
 )
 

+ 1 - 1
server/proxy/p2p.go

@@ -5,8 +5,8 @@ import (
 	"strings"
 	"time"
 
+	"ehang.io/nps/lib/common"
 	"github.com/astaxie/beego/logs"
-	"github.com/cnlh/nps/lib/common"
 )
 
 type P2PServer struct {

+ 122 - 20
server/proxy/socks5.go

@@ -7,10 +7,10 @@ import (
 	"net"
 	"strconv"
 
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/conn"
+	"ehang.io/nps/lib/file"
 	"github.com/astaxie/beego/logs"
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/conn"
-	"github.com/cnlh/nps/lib/file"
 )
 
 const (
@@ -154,27 +154,129 @@ func (s *Sock5ModeServer) handleConnect(c net.Conn) {
 // passive mode
 func (s *Sock5ModeServer) handleBind(c net.Conn) {
 }
+func (s *Sock5ModeServer) sendUdpReply(writeConn net.Conn, c net.Conn, rep uint8, serverIp string) {
+	reply := []byte{
+		5,
+		rep,
+		0,
+		1,
+	}
+	localHost, localPort, _ := net.SplitHostPort(c.LocalAddr().String())
+	localHost = serverIp
+	ipBytes := net.ParseIP(localHost).To4()
+	nPort, _ := strconv.Atoi(localPort)
+	reply = append(reply, ipBytes...)
+	portBytes := make([]byte, 2)
+	binary.BigEndian.PutUint16(portBytes, uint16(nPort))
+	reply = append(reply, portBytes...)
+	writeConn.Write(reply)
+
+}
 
-//udp
 func (s *Sock5ModeServer) handleUDP(c net.Conn) {
-	/*
-	   +----+------+------+----------+----------+----------+
-	   |RSV | FRAG | ATYP | DST.ADDR | DST.PORT |   DATA   |
-	   +----+------+------+----------+----------+----------+
-	   | 2  |  1   |  1   | Variable |    2     | Variable |
-	   +----+------+------+----------+----------+----------+
-	*/
-	buf := make([]byte, 3)
-	c.Read(buf)
-	// relay udp datagram silently, without any notification to the requesting client
-	if buf[2] != 0 {
-		// does not support fragmentation, drop it
-		logs.Warn("does not support fragmentation, drop")
-		dummy := make([]byte, maxUDPPacketSize)
-		c.Read(dummy)
+	defer c.Close()
+	addrType := make([]byte, 1)
+	c.Read(addrType)
+	var host string
+	switch addrType[0] {
+	case ipV4:
+		ipv4 := make(net.IP, net.IPv4len)
+		c.Read(ipv4)
+		host = ipv4.String()
+	case ipV6:
+		ipv6 := make(net.IP, net.IPv6len)
+		c.Read(ipv6)
+		host = ipv6.String()
+	case domainName:
+		var domainLen uint8
+		binary.Read(c, binary.BigEndian, &domainLen)
+		domain := make([]byte, domainLen)
+		c.Read(domain)
+		host = string(domain)
+	default:
+		s.sendReply(c, addrTypeNotSupported)
+		return
 	}
+	//读取端口
+	var port uint16
+	binary.Read(c, binary.BigEndian, &port)
+	logs.Warn(host, string(port))
+	replyAddr, err := net.ResolveUDPAddr("udp", s.task.ServerIp+":0")
+	if err != nil {
+		logs.Error("build local reply addr error", err)
+		return
+	}
+	reply, err := net.ListenUDP("udp", replyAddr)
+	if err != nil {
+		s.sendReply(c, addrTypeNotSupported)
+		logs.Error("listen local reply udp port error")
+		return
+	}
+	// reply the local addr
+	s.sendUdpReply(c, reply, succeeded, common.GetServerIpByClientIp(c.RemoteAddr().(*net.TCPAddr).IP))
+	defer reply.Close()
+	// new a tunnel to client
+	link := conn.NewLink("udp5", "", s.task.Client.Cnf.Crypt, s.task.Client.Cnf.Compress, c.RemoteAddr().String(), false)
+	target, err := s.bridge.SendLinkInfo(s.task.Client.Id, link, s.task)
+	if err != nil {
+		logs.Warn("get connection from client id %d  error %s", s.task.Client.Id, err.Error())
+		return
+	}
+
+	var clientAddr net.Addr
+	// copy buffer
+	go func() {
+		b := common.BufPoolUdp.Get().([]byte)
+		defer common.BufPoolUdp.Put(b)
+		defer c.Close()
+
+		for {
+			n, laddr, err := reply.ReadFrom(b)
+			if err != nil {
+				logs.Error("read data from %s err %s", reply.LocalAddr().String(), err.Error())
+				return
+			}
+			if clientAddr == nil {
+				clientAddr = laddr
+			}
+			if _, err := target.Write(b[:n]); err != nil {
+				logs.Error("write data to client error", err.Error())
+				return
+			}
+		}
+	}()
+
+	go func() {
+		var l int32
+		b := common.BufPoolUdp.Get().([]byte)
+		defer common.BufPoolUdp.Put(b)
+		defer c.Close()
+		for {
+			if err := binary.Read(target, binary.LittleEndian, &l); err != nil || l >= common.PoolSizeUdp || l <= 0 {
+				logs.Warn("read len bytes error", err.Error())
+				return
+			}
+			binary.Read(target, binary.LittleEndian, b[:l])
+			if err != nil {
+				logs.Warn("read data form client error", err.Error())
+				return
+			}
+			if _, err := reply.WriteTo(b[:l], clientAddr); err != nil {
+				logs.Warn("write data to user ", err.Error())
+				return
+			}
+		}
+	}()
 
-	s.doConnect(c, associateMethod)
+	b := common.BufPoolUdp.Get().([]byte)
+	defer common.BufPoolUdp.Put(b)
+	for {
+		_, err := c.Read(b)
+		if err != nil {
+			c.Close()
+			return
+		}
+	}
 }
 
 //new conn

+ 17 - 9
server/proxy/tcp.go

@@ -7,13 +7,13 @@ import (
 	"path/filepath"
 	"strconv"
 
+	"ehang.io/nps/bridge"
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/conn"
+	"ehang.io/nps/lib/file"
+	"ehang.io/nps/server/connection"
 	"github.com/astaxie/beego"
 	"github.com/astaxie/beego/logs"
-	"github.com/cnlh/nps/bridge"
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/conn"
-	"github.com/cnlh/nps/lib/file"
-	"github.com/cnlh/nps/server/connection"
 )
 
 type TunnelModeServer struct {
@@ -63,15 +63,23 @@ func (s *WebServer) Start() error {
 		<-stop
 	}
 	beego.BConfig.WebConfig.Session.SessionOn = true
-	beego.SetStaticPath("/static", filepath.Join(common.GetRunPath(), "web", "static"))
+	beego.SetStaticPath(beego.AppConfig.String("web_base_url")+"/static", filepath.Join(common.GetRunPath(), "web", "static"))
 	beego.SetViewsPath(filepath.Join(common.GetRunPath(), "web", "views"))
-	if l, err := connection.GetWebManagerListener(); err == nil {
+	err := errors.New("Web management startup failure ")
+	var l net.Listener
+	if l, err = connection.GetWebManagerListener(); err == nil {
 		beego.InitBeforeHTTPRun()
-		http.Serve(l, beego.BeeApp.Handlers)
+		if beego.AppConfig.String("web_open_ssl") == "true" {
+			keyPath := beego.AppConfig.String("web_key_file")
+			certPath := beego.AppConfig.String("web_cert_file")
+			err = http.ServeTLS(l, beego.BeeApp.Handlers, certPath, keyPath)
+		} else {
+			err = http.Serve(l, beego.BeeApp.Handlers)
+		}
 	} else {
 		logs.Error(err)
 	}
-	return errors.New("Web management startup failure")
+	return err
 }
 
 func (s *WebServer) Close() error {

+ 2 - 2
server/proxy/transport.go

@@ -7,8 +7,8 @@ import (
 	"strconv"
 	"syscall"
 
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/conn"
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/conn"
 )
 
 func HandleTrans(c *conn.Conn, s *TunnelModeServer) error {

+ 1 - 1
server/proxy/transport_windows.go

@@ -3,7 +3,7 @@
 package proxy
 
 import (
-	"github.com/cnlh/nps/lib/conn"
+	"ehang.io/nps/lib/conn"
 )
 
 func HandleTrans(c *conn.Conn, s *TunnelModeServer) error {

+ 4 - 4
server/proxy/udp.go

@@ -4,11 +4,11 @@ import (
 	"net"
 	"strings"
 
+	"ehang.io/nps/bridge"
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/conn"
+	"ehang.io/nps/lib/file"
 	"github.com/astaxie/beego/logs"
-	"github.com/cnlh/nps/bridge"
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/conn"
-	"github.com/cnlh/nps/lib/file"
 )
 
 type UdpModeServer struct {

+ 15 - 7
server/server.go

@@ -1,6 +1,7 @@
 package server
 
 import (
+	"ehang.io/nps/lib/version"
 	"errors"
 	"math"
 	"os"
@@ -8,13 +9,13 @@ import (
 	"strings"
 	"time"
 
+	"ehang.io/nps/bridge"
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/file"
+	"ehang.io/nps/server/proxy"
+	"ehang.io/nps/server/tool"
 	"github.com/astaxie/beego"
 	"github.com/astaxie/beego/logs"
-	"github.com/cnlh/nps/bridge"
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/file"
-	"github.com/cnlh/nps/server/proxy"
-	"github.com/cnlh/nps/server/tool"
 	"github.com/shirou/gopsutil/cpu"
 	"github.com/shirou/gopsutil/load"
 	"github.com/shirou/gopsutil/mem"
@@ -109,6 +110,7 @@ func StartNewServer(bridgePort int, cnf *file.Tunnel, bridgeType string) {
 
 func dealClientFlow() {
 	ticker := time.NewTicker(time.Minute)
+	defer ticker.Stop()
 	for {
 		select {
 		case <-ticker.C:
@@ -270,8 +272,9 @@ func GetClientList(start, length int, search, sort, order string, clientId int)
 func dealClientData() {
 	file.GetDb().JsonDb.Clients.Range(func(key, value interface{}) bool {
 		v := value.(*file.Client)
-		if _, ok := Bridge.Client.Load(v.Id); ok {
+		if vv, ok := Bridge.Client.Load(v.Id); ok {
 			v.IsConnect = true
+			v.Version = vv.(*bridge.Client).Version
 		} else {
 			v.IsConnect = false
 		}
@@ -337,8 +340,12 @@ func DelClientConnect(clientId int) {
 
 func GetDashboardData() map[string]interface{} {
 	data := make(map[string]interface{})
+	data["version"] = version.VERSION
 	data["hostCount"] = common.GeSynctMapLen(file.GetDb().JsonDb.Hosts)
-	data["clientCount"] = common.GeSynctMapLen(file.GetDb().JsonDb.Clients) - 1 //Remove the public key client
+	data["clientCount"] = common.GeSynctMapLen(file.GetDb().JsonDb.Clients)
+	if beego.AppConfig.String("public_vkey") != "" { //remove public vkey
+		data["clientCount"] = data["clientCount"].(int) - 1
+	}
 	dealClientData()
 	c := 0
 	var in, out int64
@@ -430,6 +437,7 @@ func GetDashboardData() map[string]interface{} {
 
 func flowSession(m time.Duration) {
 	ticker := time.NewTicker(m)
+	defer ticker.Stop()
 	for {
 		select {
 		case <-ticker.C:

+ 2 - 2
server/test/test.go

@@ -5,9 +5,9 @@ import (
 	"path/filepath"
 	"strconv"
 
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/file"
 	"github.com/astaxie/beego"
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/file"
 )
 
 func TestServerConfig() {

+ 4 - 1
server/tool/utils.go

@@ -5,8 +5,8 @@ import (
 	"strconv"
 	"time"
 
+	"ehang.io/nps/lib/common"
 	"github.com/astaxie/beego"
-	"github.com/cnlh/nps/lib/common"
 	"github.com/shirou/gopsutil/cpu"
 	"github.com/shirou/gopsutil/load"
 	"github.com/shirou/gopsutil/mem"
@@ -31,6 +31,9 @@ func InitAllowPort() {
 }
 
 func TestServerPort(p int, m string) (b bool) {
+	if m == "p2p" || m == "secret" {
+		return true
+	}
 	if p > 65535 || p < 0 {
 		return false
 	}

+ 1 - 1
web/controllers/auth.go

@@ -4,8 +4,8 @@ import (
 	"encoding/hex"
 	"time"
 
+	"ehang.io/nps/lib/crypt"
 	"github.com/astaxie/beego"
-	"github.com/cnlh/nps/lib/crypt"
 )
 
 type AuthController struct {

+ 8 - 5
web/controllers/base.go

@@ -7,11 +7,11 @@ import (
 	"strings"
 	"time"
 
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/crypt"
+	"ehang.io/nps/lib/file"
+	"ehang.io/nps/server"
 	"github.com/astaxie/beego"
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/crypt"
-	"github.com/cnlh/nps/lib/file"
-	"github.com/cnlh/nps/server"
 )
 
 type BaseController struct {
@@ -22,6 +22,7 @@ type BaseController struct {
 
 //初始化参数
 func (s *BaseController) Prepare() {
+	s.Data["web_base_url"] = beego.AppConfig.String("web_base_url")
 	controllerName, actionName := s.GetControllerAndAction()
 	s.controllerName = strings.ToLower(controllerName[0 : len(controllerName)-10])
 	s.actionName = strings.ToLower(actionName)
@@ -34,7 +35,7 @@ func (s *BaseController) Prepare() {
 	timeNowUnix := time.Now().Unix()
 	if !((math.Abs(float64(timeNowUnix-int64(timestamp))) <= 20) && (crypt.Md5(configKey+strconv.Itoa(timestamp)) == md5Key)) {
 		if s.GetSession("auth") != true {
-			s.Redirect("/login/index", 302)
+			s.Redirect(beego.AppConfig.String("web_base_url")+"/login/index", 302)
 		}
 	}
 	if s.GetSession("isAdmin") != nil && !s.GetSession("isAdmin").(bool) {
@@ -60,6 +61,7 @@ func (s *BaseController) Prepare() {
 
 //加载模板
 func (s *BaseController) display(tpl ...string) {
+	s.Data["web_base_url"] = beego.AppConfig.String("web_base_url")
 	var tplname string
 	if s.Data["menu"] == nil {
 		s.Data["menu"] = s.actionName
@@ -83,6 +85,7 @@ func (s *BaseController) display(tpl ...string) {
 
 //错误
 func (s *BaseController) error() {
+	s.Data["web_base_url"] = beego.AppConfig.String("web_base_url")
 	s.Layout = "public/layout.html"
 	s.TplName = "public/error.html"
 }

+ 4 - 4
web/controllers/client.go

@@ -1,11 +1,11 @@
 package controllers
 
 import (
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/file"
+	"ehang.io/nps/lib/rate"
+	"ehang.io/nps/server"
 	"github.com/astaxie/beego"
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/file"
-	"github.com/cnlh/nps/lib/rate"
-	"github.com/cnlh/nps/server"
 )
 
 type ClientController struct {

+ 6 - 3
web/controllers/index.go

@@ -1,9 +1,11 @@
 package controllers
 
 import (
-	"github.com/cnlh/nps/lib/file"
-	"github.com/cnlh/nps/server"
-	"github.com/cnlh/nps/server/tool"
+	"ehang.io/nps/lib/file"
+	"ehang.io/nps/server"
+	"ehang.io/nps/server/tool"
+
+	"github.com/astaxie/beego"
 )
 
 type IndexController struct {
@@ -11,6 +13,7 @@ type IndexController struct {
 }
 
 func (s *IndexController) Index() {
+	s.Data["web_base_url"] = beego.AppConfig.String("web_base_url")
 	s.Data["data"] = server.GetDashboardData()
 	s.SetInfo("dashboard")
 	s.display("index/index")

+ 52 - 4
web/controllers/login.go

@@ -1,26 +1,52 @@
 package controllers
 
 import (
+	"math/rand"
+	"net"
+	"sync"
 	"time"
 
+	"ehang.io/nps/lib/common"
+	"ehang.io/nps/lib/file"
+	"ehang.io/nps/server"
 	"github.com/astaxie/beego"
-	"github.com/cnlh/nps/lib/common"
-	"github.com/cnlh/nps/lib/file"
-	"github.com/cnlh/nps/server"
 )
 
 type LoginController struct {
 	beego.Controller
 }
 
+var ipRecord sync.Map
+
+type record struct {
+	hasLoginFailTimes int
+	lastLoginTime     time.Time
+}
+
 func (self *LoginController) Index() {
+	self.Data["web_base_url"] = beego.AppConfig.String("web_base_url")
 	self.Data["register_allow"], _ = beego.AppConfig.Bool("allow_user_register")
 	self.TplName = "login/index.html"
 }
 func (self *LoginController) Verify() {
+	clearIprecord()
+	ip, _, _ := net.SplitHostPort(self.Ctx.Request.RemoteAddr)
+	if v, ok := ipRecord.Load(ip); ok {
+		vv := v.(*record)
+		if (time.Now().Unix() - vv.lastLoginTime.Unix()) >= 60 {
+			vv.hasLoginFailTimes = 0
+		}
+		if vv.hasLoginFailTimes >= 10 {
+			self.Data["json"] = map[string]interface{}{"status": 0, "msg": "username or password incorrect"}
+			self.ServeJSON()
+			return
+		}
+	}
 	var auth bool
 	if self.GetString("password") == beego.AppConfig.String("web_password") && self.GetString("username") == beego.AppConfig.String("web_username") {
 		self.SetSession("isAdmin", true)
+		self.DelSession("clientId")
+		self.DelSession("username")
 		auth = true
 		server.Bridge.Register.Store(common.GetIpByAddr(self.Ctx.Input.IP()), time.Now().Add(time.Hour*time.Duration(2)))
 	}
@@ -53,13 +79,21 @@ func (self *LoginController) Verify() {
 	if auth {
 		self.SetSession("auth", true)
 		self.Data["json"] = map[string]interface{}{"status": 1, "msg": "login success"}
+		ipRecord.Delete(ip)
 	} else {
+		if v, load := ipRecord.LoadOrStore(ip, &record{hasLoginFailTimes: 1, lastLoginTime: time.Now()}); load {
+			vv := v.(*record)
+			vv.lastLoginTime = time.Now()
+			vv.hasLoginFailTimes += 1
+			ipRecord.Store(ip, vv)
+		}
 		self.Data["json"] = map[string]interface{}{"status": 0, "msg": "username or password incorrect"}
 	}
 	self.ServeJSON()
 }
 func (self *LoginController) Register() {
 	if self.Ctx.Request.Method == "GET" {
+		self.Data["web_base_url"] = beego.AppConfig.String("web_base_url")
 		self.TplName = "login/register.html"
 	} else {
 		if b, err := beego.AppConfig.Bool("allow_user_register"); err != nil || !b {
@@ -91,5 +125,19 @@ func (self *LoginController) Register() {
 
 func (self *LoginController) Out() {
 	self.SetSession("auth", false)
-	self.Redirect("/login/index", 302)
+	self.Redirect(beego.AppConfig.String("web_base_url")+"/login/index", 302)
+}
+
+func clearIprecord() {
+	rand.Seed(time.Now().UnixNano())
+	x := rand.Intn(100)
+	if x == 1 {
+		ipRecord.Range(func(key, value interface{}) bool {
+			v := value.(*record)
+			if time.Now().Unix()-v.lastLoginTime.Unix() >= 60 {
+				ipRecord.Delete(key)
+			}
+			return true
+		})
+	}
 }

+ 19 - 7
web/routers/router.go

@@ -1,14 +1,26 @@
 package routers
 
 import (
+	"ehang.io/nps/web/controllers"
 	"github.com/astaxie/beego"
-	"github.com/cnlh/nps/web/controllers"
 )
 
-func init() {
-	beego.Router("/", &controllers.IndexController{}, "*:Index")
-	beego.AutoRouter(&controllers.IndexController{})
-	beego.AutoRouter(&controllers.LoginController{})
-	beego.AutoRouter(&controllers.ClientController{})
-	beego.AutoRouter(&controllers.AuthController{})
+func Init() {
+	web_base_url := beego.AppConfig.String("web_base_url")
+	if len(web_base_url) > 0 {
+		ns := beego.NewNamespace(web_base_url,
+			beego.NSRouter("/", &controllers.IndexController{}, "*:Index"),
+			beego.NSAutoRouter(&controllers.IndexController{}),
+			beego.NSAutoRouter(&controllers.LoginController{}),
+			beego.NSAutoRouter(&controllers.ClientController{}),
+			beego.NSAutoRouter(&controllers.AuthController{}),
+		)
+		beego.AddNamespace(ns)
+	} else {
+		beego.Router("/", &controllers.IndexController{}, "*:Index")
+		beego.AutoRouter(&controllers.IndexController{})
+		beego.AutoRouter(&controllers.LoginController{})
+		beego.AutoRouter(&controllers.ClientController{})
+		beego.AutoRouter(&controllers.AuthController{})
+	}
 }

+ 2 - 2
web/static/js/langchange.js

@@ -12,7 +12,7 @@
 
         $.ajax({
             type: "GET",
-            url: defaults.file,
+            url: window.nps.web_base_url + defaults.file,
             dataType: "xml",
             success: function (xml) {
                 $(xml).find('text').each(function () {
@@ -65,4 +65,4 @@ $(document).ready(function () {
         setCookie("lang", "zh")
         $("body").cloudLang({lang: "zh", file: "/static/page/lang-example.xml"});
     });
-});
+});

+ 1 - 1
web/static/page/error.html

@@ -5,6 +5,6 @@
     <title>nps error</title>
 </head>
 <body>
-404 not found,power by <a href="//github.com/cnlh/nps">nps</a>
+404 not found,power by <a href="//ehang.io/nps">nps</a>
 </body>
 </html>

+ 189 - 0
web/static/page/lang-example.xml

@@ -0,0 +1,189 @@
+<content>
+    <text id="menu-dashboard">
+        <zh>仪表盘</zh>
+        <en>dashboard</en>
+    </text>
+    <text id="menu-client">
+        <zh>客户端</zh>
+        <en>client</en>
+    </text>
+    <text id="menu-host">
+        <zh>域名解析</zh>
+        <en>host</en>
+    </text>
+    <text id="menu-tcp">
+        <zh>tcp隧道</zh>
+        <en>tcp</en>
+    </text>
+    <text id="menu-udp">
+        <zh>udp隧道</zh>
+        <en>udp</en>
+    </text>
+    <text id="menu-http">
+        <zh>http代理</zh>
+        <en>http</en>
+    </text>
+    <text id="menu-socks5">
+        <zh>socks5代理</zh>
+        <en>socks5</en>
+    </text>
+    <text id="menu-secret">
+        <zh>私密代理</zh>
+        <en>secret</en>
+    </text>
+    <text id="menu-p2p">
+        <zh>p2p代理</zh>
+        <en>p2p</en>
+    </text>
+    <text id="menu-file">
+        <zh>文件代理</zh>
+        <en>file</en>
+    </text>
+    <text id="info-remark">
+        <zh>备注</zh>
+        <en>remark</en>
+    </text>
+    <text id="info-flow-limit">
+        <zh>流量限制</zh>
+        <en>flow limit</en>
+    </text>
+    <text id="info-bandwidth">
+        <zh>带宽限制</zh>
+        <en>bandwidth</en>
+    </text>
+    <text id="info-max-conn-num">
+        <zh>最大连接数限制</zh>
+        <en>maximum number of client connections
+        </en>
+    </text>
+    <text id="info-web-auth-username">
+        <zh>basic权限验证用户名</zh>
+        <en>web authentication username</en>
+    </text>
+    <text id="info-web-auth-password">
+        <zh>basic权限验证密码</zh>
+        <en>web authentication password</en>
+    </text>
+    <text id="info-client-vkey">
+        <zh>客户端连接密钥</zh>
+        <en>client connection key</en>
+    </text>
+    <text id="info-compress">
+        <zh>压缩</zh>
+        <en>compress</en>
+    </text>
+    <text id="info-crypt">
+        <zh>加密</zh>
+        <en>crypt</en>
+    </text>
+
+
+    <text id="info-host">
+        <zh>域名</zh>
+        <en>host</en>
+    </text>
+    <text id="info-scheme">
+        <zh>协议类型</zh>
+        <en>scheme</en>
+    </text>
+    <text id="info-url-router">
+        <zh>url路由</zh>
+        <en>url router</en>
+    </text>
+    <text id="info-client-id">
+        <zh>客户端id</zh>
+        <en>client id</en>
+    </text>
+    <text id="info-target">
+        <zh>内网目标(ip:端口)</zh>
+        <en>target of Intranet(ip:port)</en>
+    </text>
+    <text id="info-header-modify">
+        <zh>request header修改</zh>
+        <en>header modify</en>
+    </text>
+    <text id="info-host-change">
+        <zh>request host修改</zh>
+        <en>host modify</en>
+    </text>
+
+
+    <text id="info-mode">
+        <zh>隧道类型</zh>
+        <en>mode</en>
+    </text>
+    <text id="info-server-port">
+        <zh>服务端端口</zh>
+        <en>server port</en>
+    </text>
+    <text id="info-server-ip">
+        <zh>服务端端口</zh>
+        <en>server ip</en>
+    </text>
+    <text id="info-crypt">
+        <zh>加密</zh>
+        <en>crypt</en>
+    </text>
+    <text id="info-local-path">
+        <zh>本地路径</zh>
+        <en>local path</en>
+    </text>
+    <text id="info-strip-pre">
+        <zh>访问前缀</zh>
+        <en>strip pre</en>
+    </text>
+    <text id="info-unique-vkey">
+        <zh>唯一识别密钥</zh>
+        <en>unique vkey</en>
+    </text>
+
+
+    <text id="info-new">
+        <zh>新增</zh>
+        <en>add</en>
+    </text>
+
+    <text id="info-now-conn-num">
+        <zh>当前连接数</zh>
+        <en>now conn num</en>
+    </text>
+    <text id="info-export-flow">
+        <en>export flow</en>
+    </text>
+    <text id="info-inlet-flow">
+        <en>inlet flow</en>
+    </text>
+    <text id="info-command">
+        <en>command</en>
+    </text>
+    <text id="info-config-conn-allow">
+        <en>allow client connect by config file</en>
+    </text>
+    <text id="info-client-web-username">
+        <en>username of web login</en>
+    </text>
+    <text id="info-client-web-password">
+        <en>password of web login</en>
+    </text>
+    <text id="info-https-cert">
+        <en>https cert file path</en>
+    </text>
+    <text id="info-https-key">
+        <en>https key file path</en>
+    </text>
+
+    <text id="info-max-tunnel-num">
+        <en>max tunnel num</en>
+    </text>
+
+    <text id="info-local-proxy">
+        <en>Is the proxy local to the server?</en>
+    </text>
+
+
+    <text id="info-save">
+        <en>save</en>
+    </text>
+
+
+</content>

+ 2 - 2
web/views/client/add.html

@@ -119,7 +119,7 @@
                     <div class="hr-line-dashed"></div>
                     <div class="form-group">
                         <div class="col-sm-4 col-sm-offset-2">
-                            <button class="btn btn-success" href="#" id="add"><i
+                            <button class="btn btn-success" type="button" id="add"><i
                                     class="fa fa-fw fa-lg fa-eye"></i>新增
                             </button>
                         </div>
@@ -134,7 +134,7 @@
         $("#add").on("click", function () {
             $.ajax({
                 type: "POST",
-                url: "/client/add",
+                url: "{{.web_base_url}}/client/add",
                 data: $("form").serializeArray(),
                 success: function (res) {
                     alert(res.msg)

+ 2 - 2
web/views/client/edit.html

@@ -128,7 +128,7 @@
                     <div class="hr-line-dashed"></div>
                     <div class="form-group">
                         <div class="col-sm-4 col-sm-offset-2">
-                            <button class="btn btn-success" href="#" id="add"><i
+                            <button class="btn btn-success" type="button" id="add"><i
                                     class="fa fa-fw fa-lg fa-eye"></i><span langtag="info-save">保存</span>
                             </button>
                         </div>
@@ -145,7 +145,7 @@
         $("#add").on("click", function () {
             $.ajax({
                 type: "POST",
-                url: "/client/edit",
+                url: "{{.web_base_url}}/client/edit",
                 data: $("form").serializeArray(),
                 success: function (res) {
                     alert(res.msg)

+ 14 - 9
web/views/client/list.html

@@ -20,7 +20,7 @@
 
                 <div class="table-responsive">
                     <div id="toolbar">
-                        <a href="/client/add" class="btn btn-primary dim" type="button" langtag="info-new">新增</a>
+                        <a href="{{.web_base_url}}/client/add" class="btn btn-primary dim" type="button" langtag="info-new">新增</a>
                     </div>
                     <table id="taskList_table" class="table-striped table-hover"
                            data-mobile-responsive="true"></table>
@@ -42,7 +42,7 @@
         if (confirm("Are you sure you want to delete it??")) {
             $.ajax({
                 type: "POST",
-                url: "/client/del",
+                url: "{{.web_base_url}}/client/del",
                 data: {"id": id},
                 success: function (res) {
                     alert(res.msg)
@@ -58,7 +58,7 @@
         if (confirm("Are you sure you want to start it??")) {
             $.ajax({
                 type: "POST",
-                url: "/client/changestatus",
+                url: "{{.web_base_url}}/client/changestatus",
                 data: {"id": id, "status": 1},
                 success: function (res) {
                     alert(res.msg)
@@ -74,7 +74,7 @@
         if (confirm("Are you sure you want to stop it?")) {
             $.ajax({
                 type: "POST",
-                url: "/client/changestatus",
+                url: "{{.web_base_url}}/client/changestatus",
                 data: {
                     "id": id, "status": 0
                 },
@@ -90,26 +90,26 @@
     }
 
     function edit(id) {
-        window.location.href = "/client/edit?id=" + id
+        window.location.href = "{{.web_base_url}}/client/edit?id=" + id
     }
 
     function add() {
-        window.location.href = "/client/add"
+        window.location.href = "{{.web_base_url}}/client/add"
     }
 
     function tunnel(id) {
-        window.location.href = "/index/all?client_id=" + id
+        window.location.href = "{{.web_base_url}}/index/all?client_id=" + id
     }
 
     function host(id) {
-        window.location.href = "/index/hostlist?client_id=" + id
+        window.location.href = "{{.web_base_url}}/index/hostlist?client_id=" + id
     }
 
     /*bootstrap table*/
     $('#table').bootstrapTable({
         toolbar: "#toolbar",
         method: 'post', // 服务器数据的请求方式 get or post
-        url: "/client/list", // 服务器数据的加载地址
+        url: "{{.web_base_url}}/client/list", // 服务器数据的加载地址
         contentType: "application/x-www-form-urlencoded",
         striped: true, // 设置为true会有隔行变色效果
         search: true,
@@ -150,6 +150,11 @@
                 title: 'remark',//标题
                 visible: true,//false表示不显示
             },
+            {
+                field: 'Version',//域值
+                title: 'version',//标题
+                visible: true,//false表示不显示
+            },
             {
                 field: 'VerifyKey',//域值
                 title: 'vkey',//标题

+ 2 - 2
web/views/index/add.html

@@ -101,7 +101,7 @@
                     <div class="hr-line-dashed"></div>
                     <div class="form-group">
                         <div class="col-sm-4 col-sm-offset-2">
-                            &nbsp;<button class="btn btn-success" href="#" id="add"><i
+                            &nbsp;<button class="btn btn-success" type="button" id="add"><i
                                 class="fa fa-fw fa-lg fa-eye"></i>新增
                         </button>
                         </div>
@@ -162,7 +162,7 @@
         $("#add").on("click", function () {
             $.ajax({
                 type: "POST",
-                url: "/index/add",
+                url: "{{.web_base_url}}/index/add",
                 data: $("form").serializeArray(),
                 success: function (res) {
                     alert(res.msg)

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.