├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ └── release.yml
├── .gitignore
├── .travis.yml
├── Dockerfile.npc
├── Dockerfile.nps
├── LICENSE
├── Makefile
├── README.md
├── README_zh.md
├── bridge
└── bridge.go
├── build.android.sh
├── build.assets.sh
├── build.sh
├── client
├── client.go
├── control.go
├── health.go
├── local.go
└── register.go
├── cmd
├── npc
│ ├── npc.go
│ └── sdk.go
└── nps
│ └── nps.go
├── conf
├── clients.json
├── hosts.json
├── multi_account.conf
├── npc.conf
├── nps.conf
├── server.key
├── server.pem
└── tasks.json
├── docs
├── .nojekyll
├── README.md
├── _coverpage.md
├── _navbar.md
├── _sidebar.md
├── api.md
├── contribute.md
├── description.md
├── discuss.md
├── donate.md
├── example.md
├── faq.md
├── feature.md
├── index.html
├── install.md
├── introduction.md
├── logo.png
├── logo.svg
├── npc_extend.md
├── npc_sdk.md
├── nps_extend.md
├── nps_use.md
├── run.md
├── server_config.md
├── thanks.md
├── use.md
├── webapi.md
└── windows_client_service_configuration.png
├── go.mod
├── go.sum
├── gui
└── npc
│ ├── AndroidManifest.xml
│ └── npc.go
├── image
├── cpu1.png
├── cpu2.png
├── donation_wx.png
├── donation_zfb.png
├── http.png
├── httpProxy.png
├── qps.png
├── sock5.png
├── speed.png
├── tcp.png
├── udp.png
├── web.png
├── web2.png
└── work_flow.svg
├── lib
├── cache
│ └── lru.go
├── common
│ ├── const.go
│ ├── logs.go
│ ├── netpackager.go
│ ├── pool.go
│ ├── pprof.go
│ ├── run.go
│ └── util.go
├── config
│ ├── config.go
│ └── config_test.go
├── conn
│ ├── conn.go
│ ├── link.go
│ ├── listener.go
│ └── snappy.go
├── crypt
│ ├── clientHello.go
│ ├── crypt.go
│ └── tls.go
├── daemon
│ ├── daemon.go
│ └── reload.go
├── file
│ ├── db.go
│ ├── file.go
│ ├── obj.go
│ └── sort.go
├── goroutine
│ └── pool.go
├── install
│ └── install.go
├── pmux
│ ├── pconn.go
│ ├── plistener.go
│ ├── pmux.go
│ └── pmux_test.go
├── rate
│ ├── conn.go
│ └── rate.go
├── sheap
│ └── heap.go
└── version
│ └── version.go
├── server
├── connection
│ └── connection.go
├── proxy
│ ├── base.go
│ ├── http.go
│ ├── https.go
│ ├── p2p.go
│ ├── socks5.go
│ ├── tcp.go
│ ├── transport.go
│ ├── transport_windows.go
│ └── udp.go
├── server.go
├── test
│ └── test.go
└── tool
│ └── utils.go
└── web
├── controllers
├── auth.go
├── base.go
├── client.go
├── index.go
└── login.go
├── routers
└── router.go
├── static
├── css
│ ├── bootstrap-table.min.css
│ ├── bootstrap.min.css
│ ├── datatables.css
│ ├── fontawesome.min.css
│ ├── regular.min.css
│ ├── solid.min.css
│ └── style.css
├── img
│ └── flag
│ │ ├── en-US.png
│ │ └── zh-CN.png
├── js
│ ├── bootstrap-table-locale-all.min.js
│ ├── bootstrap-table.min.js
│ ├── bootstrap.min.js
│ ├── echarts.min.js
│ ├── fontawesome.min.js
│ ├── inspinia.js
│ ├── jquery-3.4.1.min.js
│ ├── language.js
│ └── popper.min.js
├── page
│ ├── error.html
│ └── languages.xml
└── webfonts
│ ├── fa-solid-900.eot
│ ├── fa-solid-900.svg
│ ├── fa-solid-900.ttf
│ ├── fa-solid-900.woff
│ └── fa-solid-900.woff2
└── views
├── client
├── add.html
├── edit.html
└── list.html
├── index
├── add.html
├── edit.html
├── hadd.html
├── hedit.html
├── help.html
├── hlist.html
├── index.html
└── list.html
├── login
├── index.html
└── register.html
└── public
├── error.html
└── layout.html
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.js linguist-language=golang
2 | *.css linguist-language=golang
3 | *.html linguist-language=golang
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Opening '...'
16 | 2. Click on '....'
17 | 3. See error
18 |
19 | **Expected behavior**
20 | A clear and concise description of what you expected to happen.
21 |
22 | **Screenshots or logs**
23 | Add screenshots or logs to help explain your problem.
24 |
25 | **Server (please complete the following information):**
26 | - OS: [e.g. Centos, Windows]
27 | - ARCH: [e.g. Amd64, Arm]
28 | - Tunnel [e.g. TCP, HTTP]
29 | - Version [e.g. 0.24.0]
30 |
31 | **Client (please complete the following information):**
32 | - OS: [e.g. Centos, Windows]
33 | - ARCH: [e.g. Amd64, Arm]
34 | - Tunnel [e.g. TCP, HTTP]
35 | - Version [e.g. 0.24.0]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | release:
5 | types: [published]
6 | branches: [ master ]
7 |
8 | jobs:
9 |
10 | build_assets:
11 |
12 | runs-on: ubuntu-latest
13 | steps:
14 |
15 | - name: Set up Go 1.x
16 | uses: actions/setup-go@v2
17 | with:
18 | go-version: 1.15
19 | id: go
20 | - name: Check out code into the Go module directory
21 | uses: actions/checkout@v2
22 | - name: Get dependencies
23 | run: |
24 | go get -v -t -d ./...
25 | if [ -f Gopkg.toml ]; then
26 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
27 | dep ensure
28 | fi
29 | - name: Build
30 | run: |
31 | chmod +x build.assets.sh
32 | ./build.assets.sh
33 | - name: Upload
34 | uses: softprops/action-gh-release@v1
35 | if: startsWith(github.ref, 'refs/tags/')
36 | with:
37 | files: |
38 | freebsd_386_client.tar.gz
39 | freebsd_386_server.tar.gz
40 | freebsd_amd64_client.tar.gz
41 | freebsd_amd64_server.tar.gz
42 | freebsd_arm_client.tar.gz
43 | freebsd_arm_server.tar.gz
44 | linux_386_client.tar.gz
45 | linux_386_server.tar.gz
46 | linux_amd64_client.tar.gz
47 | linux_amd64_server.tar.gz
48 | linux_arm64_client.tar.gz
49 | linux_arm64_server.tar.gz
50 | linux_arm_v5_client.tar.gz
51 | linux_arm_v6_client.tar.gz
52 | linux_arm_v7_client.tar.gz
53 | linux_arm_v5_server.tar.gz
54 | linux_arm_v6_server.tar.gz
55 | linux_arm_v7_server.tar.gz
56 | linux_mips64le_client.tar.gz
57 | linux_mips64le_server.tar.gz
58 | linux_mips64_client.tar.gz
59 | linux_mips64_server.tar.gz
60 | linux_mipsle_client.tar.gz
61 | linux_mipsle_server.tar.gz
62 | linux_mips_client.tar.gz
63 | linux_mips_server.tar.gz
64 | darwin_amd64_client.tar.gz
65 | darwin_amd64_server.tar.gz
66 | windows_386_client.tar.gz
67 | windows_386_server.tar.gz
68 | windows_amd64_client.tar.gz
69 | windows_amd64_server.tar.gz
70 | npc_sdk.tar.gz
71 | env:
72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
73 |
74 | build_android:
75 |
76 | runs-on: ubuntu-latest
77 | steps:
78 |
79 | - name: Check out code into the Go module directory
80 | uses: actions/checkout@v2
81 | - name: Build
82 | run: |
83 | chmod +x build.android.sh
84 | docker run --rm -i -w /app -v $(pwd):/app -e ANDROID_HOME=/usr/local/android_sdk -e GOPROXY=direct fyneio/fyne-cross:android-latest /app/build.android.sh
85 | - name: Upload
86 | uses: softprops/action-gh-release@v1
87 | if: startsWith(github.ref, 'refs/tags/')
88 | with:
89 | files: |
90 | android_client.apk
91 | env:
92 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
93 |
94 | build_spk:
95 |
96 | runs-on: ubuntu-latest
97 | steps:
98 |
99 | - name: Check out code into the Go module directory
100 | uses: actions/checkout@v2
101 | - name: Set env
102 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
103 | - name: Build
104 | run: |
105 | git clone https://github.com/cnlh/spksrc.git ~/spksrc
106 | mkdir ~/spksrc/nps && cp -rf ./* ~/spksrc/nps/
107 | docker run -id --name spksrc --env VERSION=${{ env.RELEASE_VERSION }} -e GOPROXY=direct -v ~/spksrc:/spksrc synocommunity/spksrc /bin/bash
108 | docker exec spksrc /bin/bash -c 'cd /spksrc && make setup && cd /spksrc/spk/npc && make'
109 | cp ~/spksrc/packages/npc_noarch-all_${{ env.RELEASE_VERSION }}-1.spk ./npc_syno.spk
110 | - name: Upload
111 | uses: softprops/action-gh-release@v1
112 | if: startsWith(github.ref, 'refs/tags/')
113 | with:
114 | files: |
115 | npc_syno.spk
116 | env:
117 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
118 |
119 | build_docker:
120 |
121 | runs-on: ubuntu-latest
122 | steps:
123 |
124 | - name: Check out code into the Go module directory
125 | uses: actions/checkout@v2
126 | - name: Set env
127 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
128 | - name: Set up QEMU
129 | uses: docker/setup-qemu-action@v1
130 | - name: Set up Docker Buildx
131 | uses: docker/setup-buildx-action@v1
132 | - name: Cache Docker layers
133 | uses: actions/cache@v2
134 | with:
135 | path: /tmp/.buildx-cache
136 | key: ${{ runner.os }}-buildx-${{ github.sha }}
137 | restore-keys: |
138 | ${{ runner.os }}-buildx-
139 | - name: Login to DockerHub
140 | uses: docker/login-action@v1
141 | with:
142 | username: ${{ secrets.DOCKERHUB_USERNAME }}
143 | password: ${{ secrets.DOCKERHUB_TOKEN }}
144 | - name: Build and push nps
145 | uses: docker/build-push-action@v2
146 | with:
147 | context: .
148 | file: ./Dockerfile.nps
149 | platforms: linux/amd64,linux/arm,linux/arm64
150 | push: true
151 | tags: |
152 | ${{ secrets.DOCKERHUB_USERNAME }}/nps:latest
153 | ${{ secrets.DOCKERHUB_USERNAME }}/nps:${{ env.RELEASE_VERSION }}
154 | - name: Build and push npc
155 | uses: docker/build-push-action@v2
156 | with:
157 | context: .
158 | file: ./Dockerfile.npc
159 | platforms: linux/amd64,linux/arm,linux/arm64
160 | push: true
161 | tags: |
162 | ${{ secrets.DOCKERHUB_USERNAME }}/npc:latest
163 | ${{ secrets.DOCKERHUB_USERNAME }}/npc:${{ env.RELEASE_VERSION }}
164 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | nps
3 | npc
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 |
3 | go:
4 | - 1.14.x
5 | services:
6 | - docker
7 | script:
8 | - GOPROXY=direct go test -v ./cmd/nps/
9 | os:
10 | - linux
11 | before_deploy:
12 | - chmod +x ./build.sh && chmod +x ./build.android.sh && ./build.sh
13 |
14 | deploy:
15 | provider: releases
16 | edge: true
17 | token: ${GH_TOKEN}
18 | cleanup: false
19 | file:
20 | - freebsd_386_client.tar.gz
21 | - freebsd_386_server.tar.gz
22 | - freebsd_amd64_client.tar.gz
23 | - freebsd_amd64_server.tar.gz
24 | - freebsd_arm_client.tar.gz
25 | - freebsd_arm_server.tar.gz
26 | - linux_386_client.tar.gz
27 | - linux_386_server.tar.gz
28 | - linux_amd64_client.tar.gz
29 | - linux_amd64_server.tar.gz
30 | - linux_arm64_client.tar.gz
31 | - linux_arm64_server.tar.gz
32 | - linux_arm_v5_client.tar.gz
33 | - linux_arm_v6_client.tar.gz
34 | - linux_arm_v7_client.tar.gz
35 | - linux_arm_v5_server.tar.gz
36 | - linux_arm_v6_server.tar.gz
37 | - linux_arm_v7_server.tar.gz
38 | - linux_mips64le_client.tar.gz
39 | - linux_mips64le_server.tar.gz
40 | - linux_mips64_client.tar.gz
41 | - linux_mips64_server.tar.gz
42 | - linux_mipsle_client.tar.gz
43 | - linux_mipsle_server.tar.gz
44 | - linux_mips_client.tar.gz
45 | - linux_mips_server.tar.gz
46 | - darwin_amd64_client.tar.gz
47 | - darwin_amd64_server.tar.gz
48 | - windows_386_client.tar.gz
49 | - windows_386_server.tar.gz
50 | - windows_amd64_client.tar.gz
51 | - windows_amd64_server.tar.gz
52 | - npc_syno.spk
53 | - npc_sdk.tar.gz
54 | - android_client.apk
55 | on:
56 | tags: true
57 | all_branches: true
58 |
--------------------------------------------------------------------------------
/Dockerfile.npc:
--------------------------------------------------------------------------------
1 | FROM golang:1.15 as builder
2 | ARG GOPROXY=direct
3 | WORKDIR /go/src/ehang.io/nps
4 | COPY . .
5 | RUN go get -d -v ./...
6 | RUN CGO_ENABLED=0 go build -ldflags="-w -s -extldflags -static" ./cmd/npc/npc.go
7 |
8 | FROM scratch
9 | COPY --from=builder /go/src/ehang.io/nps/npc /
10 | VOLUME /conf
11 | ENTRYPOINT ["/npc"]
12 |
--------------------------------------------------------------------------------
/Dockerfile.nps:
--------------------------------------------------------------------------------
1 | FROM golang:1.15 as builder
2 | ARG GOPROXY=direct
3 | WORKDIR /go/src/ehang.io/nps
4 | COPY . .
5 | RUN go get -d -v ./...
6 | RUN CGO_ENABLED=0 go build -ldflags="-w -s -extldflags -static" ./cmd/nps/nps.go
7 |
8 | FROM scratch
9 | COPY --from=builder /go/src/ehang.io/nps/nps /
10 | COPY --from=builder /go/src/ehang.io/nps/web /web
11 | VOLUME /conf
12 | CMD ["/nps"]
13 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SOURCE_FILES?=./...
2 | TEST_PATTERN?=.
3 | TEST_OPTIONS?=
4 |
5 | export PATH := ./bin:$(PATH)
6 | export GO111MODULE := on
7 | export GOPROXY := https://gocenter.io
8 |
9 | # Build a beta version of goreleaser
10 | build:
11 | go build cmd/nps/nps.go
12 | go build cmd/npc/npc.go
13 | .PHONY: build
14 |
15 | # Install all the build and lint dependencies
16 | setup:
17 | curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh
18 | curl -L https://git.io/misspell | sh
19 | go mod download
20 | .PHONY: setup
21 |
22 | # Run all the tests
23 | test:
24 | go test $(TEST_OPTIONS) -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=2m
25 | .PHONY: test
26 |
27 | # Run all the tests and opens the coverage report
28 | cover: test
29 | go tool cover -html=coverage.txt
30 | .PHONY: cover
31 |
32 | # gofmt and goimports all go files
33 | fmt:
34 | find . -name '*.go' -not -wholename './vendor/*' | while read -r file; do gofmt -w -s "$$file"; goimports -w "$$file"; done
35 | .PHONY: fmt
36 |
37 | # Run all the linters
38 | lint:
39 | # TODO: fix tests and lll issues
40 | ./bin/golangci-lint run --tests=false --enable-all --disable=lll ./...
41 | ./bin/misspell -error **/*
42 | .PHONY: lint
43 |
44 | # Clean go.mod
45 | go-mod-tidy:
46 | @go mod tidy -v
47 | @git diff HEAD
48 | @git diff-index --quiet HEAD
49 | .PHONY: go-mod-tidy
50 |
51 | # Run all the tests and code checks
52 | ci: build test lint go-mod-tidy
53 | .PHONY: ci
54 |
55 | # Generate the static documentation
56 | static:
57 | @hugo --enableGitInfo --source www
58 | .PHONY: static
59 |
60 | # Show to-do items per file.
61 | todo:
62 | @grep \
63 | --exclude-dir=vendor \
64 | --exclude-dir=node_modules \
65 | --exclude=Makefile \
66 | --text \
67 | --color \
68 | -nRo -E ' TODO:.*|SkipNow' .
69 | .PHONY: todo
70 |
71 | clean:
72 | rm npc nps
73 | .PHONY: clean
74 |
75 | .DEFAULT_GOAL := build
76 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # NPS
3 |  
4 | [](https://gitter.im/cnlh-nps/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
5 | 
6 | 
7 |
8 | [README](https://github.com/ehang-io/nps/blob/master/README.md)|[中文文档](https://github.com/ehang-io/nps/blob/master/README_zh.md)
9 |
10 | NPS is a lightweight, high-performance, powerful **intranet penetration** proxy server, with a powerful web management terminal.
11 |
12 |
13 | 
14 |
15 | ## Feature
16 |
17 | - Comprehensive protocol support, compatible with almost all commonly used protocols, such as tcp, udp, http(s), socks5, p2p, http proxy ...
18 | - Full platform compatibility (linux, windows, macos, Synology, etc.), support installation as a system service simply.
19 | - Comprehensive control, both client and server control are allowed.
20 | - Https integration, support to convert backend proxy and web services to https, and support multiple certificates.
21 | - Just simple configuration on web ui can complete most requirements.
22 | - Complete information display, such as traffic, system information, real-time bandwidth, client version, etc.
23 | - Powerful extension functions, everything is available (cache, compression, encryption, traffic limit, bandwidth limit, port reuse, etc.)
24 | - Domain name resolution has functions such as custom headers, 404 page configuration, host modification, site protection, URL routing, and pan-resolution.
25 | - Multi-user and user registration support on server.
26 |
27 | **Didn't find the feature you want? It doesn't matter, click [Enter the document](https://ehang-io.github.io/nps/) to find it!**
28 |
29 | ## Quick start
30 |
31 | ### Installation
32 |
33 | > [releases](https://github.com/ehang-io/nps/releases)
34 |
35 | Download the corresponding system version, the server and client are separate.
36 |
37 | ### Server start
38 |
39 | After downloading the server compressed package, unzip it, and then enter the unzipped folder.
40 |
41 | - execute installation command
42 |
43 | For linux、darwin ```sudo ./nps install```
44 |
45 | For windows, run cmd as administrator and enter the installation directory ```nps.exe install```
46 |
47 | - default ports
48 |
49 | The default configuration file of nps use 80,443,8080,8024 ports
50 |
51 | 80 and 443 ports for host mode default ports
52 |
53 | 8080 for web management access port
54 |
55 | 8024 for net bridge port, to communicate between server and client
56 |
57 | - start up
58 |
59 | For linux、darwin ```sudo nps start```
60 |
61 | For windows, run cmd as administrator and enter the program directory ```nps.exe start```
62 |
63 | ```After installation, the windows configuration file is located at C:\Program Files\nps, linux or darwin is located at /etc/nps```
64 |
65 | **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).**
66 |
67 | - Access server IP:web service port (default is 8080).
68 | - Login with username and password (default is admin/123, must be modified when officially used).
69 | - Create a client.
70 |
71 | ### Client connection
72 | - Click the + sign in front of the client in web management and copy the startup command.
73 | - Execute the startup command, Linux can be executed directly, Windows will replace ./npc with npc.exe and execute it with cmd.
74 |
75 |
76 | 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=注册到系统服务)
77 |
78 | ### Configuration
79 | - After the client connects, configure the corresponding penetration service in the web.
80 | - For more advanced usage, see [Complete Documentation](https://ehang-io.github.io/nps/)
81 |
82 | ## Contribution
83 | - If you encounter a bug, you can submit it to the dev branch directly.
84 | - If you encounter a problem, you can feedback through the issue.
85 | - 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.
86 | - If there is feedback on new features, you can feedback via issues or qq group.
87 |
--------------------------------------------------------------------------------
/README_zh.md:
--------------------------------------------------------------------------------
1 |
2 | # nps
3 |  
4 | [](https://gitter.im/cnlh-nps/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
5 | 
6 | 
7 |
8 | [README](https://github.com/ehang-io/nps/blob/master/README.md)|[中文文档](https://github.com/ehang-io/nps/blob/master/README_zh.md)
9 |
10 | nps是一款轻量级、高性能、功能强大的**内网穿透**代理服务器。目前支持**tcp、udp流量转发**,可支持任何**tcp、udp**上层协议(访问内网网站、本地支付接口调试、ssh访问、远程桌面,内网dns解析等等……),此外还**支持内网http代理、内网socks5代理**、**p2p等**,并带有功能强大的web管理端。
11 |
12 |
13 | ## 背景
14 | 
15 |
16 | 1. 做微信公众号开发、小程序开发等----> 域名代理模式
17 |
18 | 2. 想在外网通过ssh连接内网的机器,做云服务器到内网服务器端口的映射,----> tcp代理模式
19 |
20 | 3. 在非内网环境下使用内网dns,或者需要通过udp访问内网机器等----> udp代理模式
21 |
22 | 4. 在外网使用HTTP代理访问内网站点----> http代理模式
23 |
24 | 5. 搭建一个内网穿透ss,在外网如同使用内网vpn一样访问内网资源或者设备----> socks5代理模式
25 | ## 特点
26 | - 协议支持全面,兼容几乎所有常用协议,例如tcp、udp、http(s)、socks5、p2p、http代理...
27 | - 全平台兼容(linux、windows、macos、群辉等),支持一键安装为系统服务
28 | - 控制全面,同时支持服务端和客户端控制
29 | - https集成,支持将后端代理和web服务转成https,同时支持多证书
30 | - 操作简单,只需简单的配置即可在web ui上完成其余操作
31 | - 展示信息全面,流量、系统信息、即时带宽、客户端版本等
32 | - 扩展功能强大,该有的都有了(缓存、压缩、加密、流量限制、带宽限制、端口复用等等)
33 | - 域名解析具备自定义header、404页面配置、host修改、站点保护、URL路由、泛解析等功能
34 | - 服务端支持多用户和用户注册功能
35 |
36 | **没找到你想要的功能?不要紧,点击[进入文档](https://ehang-io.github.io/nps)查找吧**
37 | ## 快速开始
38 |
39 | ### 安装
40 | > [releases](https://github.com/ehang-io/nps/releases)
41 |
42 | 下载对应的系统版本即可,服务端和客户端是单独的
43 |
44 | ### 服务端启动
45 | 下载完服务器压缩包后,解压,然后进入解压后的文件夹
46 |
47 | - 执行安装命令
48 |
49 | 对于linux|darwin ```sudo ./nps install```
50 |
51 | 对于windows,管理员身份运行cmd,进入安装目录 ```nps.exe install```
52 |
53 | - 默认端口
54 |
55 | nps默认配置文件使用了80,443,8080,8024端口
56 |
57 | 80与443端口为域名解析模式默认端口
58 |
59 | 8080为web管理访问端口
60 |
61 | 8024为网桥端口,用于客户端与服务器通信
62 |
63 | - 启动
64 |
65 | 对于linux|darwin ```sudo nps start```
66 |
67 | 对于windows,管理员身份运行cmd,进入程序目录 ```nps.exe start```
68 |
69 | ```安装后windows配置文件位于 C:\Program Files\nps,linux和darwin位于/etc/nps```
70 |
71 | **如果发现没有启动成功,可以查看日志(Windows日志文件位于当前运行目录下,linux和darwin位于/var/log/nps.log)**
72 | - 访问服务端ip:web服务端口(默认为8080)
73 | - 使用用户名和密码登陆(默认admin/123,正式使用一定要更改)
74 | - 创建客户端
75 |
76 | ### 客户端连接
77 | - 点击web管理中客户端前的+号,复制启动命令
78 | - 执行启动命令,linux直接执行即可,windows将./npc换成npc.exe用cmd执行
79 |
80 | 如果需要注册到系统服务可查看[注册到系统服务](https://ehang-io.github.io/nps/#/use?id=注册到系统服务)
81 |
82 | ### 配置
83 | - 客户端连接后,在web中配置对应穿透服务即可
84 | - 更多高级用法见[完整文档](https://ehang-io.github.io/nps/)
85 |
86 | ## 贡献
87 | - 如果遇到bug可以直接提交至dev分支
88 | - 使用遇到问题可以通过issues反馈
89 | - 项目处于开发阶段,还有很多待完善的地方,如果可以贡献代码,请提交 PR 至 dev 分支
90 | - 如果有新的功能特性反馈,可以通过issues或者qq群反馈
91 |
--------------------------------------------------------------------------------
/build.android.sh:
--------------------------------------------------------------------------------
1 | #/bin/bash
2 |
3 | cd /go
4 | apt-get install libegl1-mesa-dev libgles2-mesa-dev libx11-dev xorg-dev -y
5 | go get -u fyne.io/fyne/v2/cmd/fyne fyne.io/fyne/v2
6 | #mkdir -p /go/src/fyne.io
7 | #cd src/fyne.io
8 | #git clone https://github.com/fyne-io/fyne.git
9 | #cd fyne
10 | #git checkout v1.2.0
11 | #go install -v ./cmd/fyne
12 | #fyne package -os android fyne.io/fyne/cmd/hello
13 | echo "fyne install success"
14 | mkdir -p /go/src/ehang.io/nps
15 | cp -R /app/* /go/src/ehang.io/nps
16 | cd /go/src/ehang.io/nps
17 | #go get -u fyne.io/fyne fyne.io/fyne/cmd/fyne
18 | rm cmd/npc/sdk.go
19 | #go get -u ./...
20 | #go mod tidy
21 | #rm -rf /go/src/golang.org/x/mobile
22 | echo "tidy success"
23 | cd /go/src/ehang.io/nps
24 | go mod vendor
25 | cd vendor
26 | cp -R * /go/src
27 | cd ..
28 | rm -rf vendor
29 | #rm -rf ~/.cache/*
30 | echo "vendor success"
31 | cd gui/npc
32 | fyne package -appID org.nps.client -os android -icon ../../docs/logo.png
33 | mv npc.apk /app/android_client.apk
34 | echo "android build success"
35 |
--------------------------------------------------------------------------------
/client/health.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "container/heap"
5 | "net"
6 | "net/http"
7 | "strings"
8 | "time"
9 |
10 | "ehang.io/nps/lib/conn"
11 | "ehang.io/nps/lib/file"
12 | "ehang.io/nps/lib/sheap"
13 | "github.com/astaxie/beego/logs"
14 | "github.com/pkg/errors"
15 | )
16 |
17 | var isStart bool
18 | var serverConn *conn.Conn
19 |
20 | func heathCheck(healths []*file.Health, c *conn.Conn) bool {
21 | serverConn = c
22 | if isStart {
23 | for _, v := range healths {
24 | v.HealthMap = make(map[string]int)
25 | }
26 | return true
27 | }
28 | isStart = true
29 | h := &sheap.IntHeap{}
30 | for _, v := range healths {
31 | if v.HealthMaxFail > 0 && v.HealthCheckTimeout > 0 && v.HealthCheckInterval > 0 {
32 | v.HealthNextTime = time.Now().Add(time.Duration(v.HealthCheckInterval) * time.Second)
33 | heap.Push(h, v.HealthNextTime.Unix())
34 | v.HealthMap = make(map[string]int)
35 | }
36 | }
37 | go session(healths, h)
38 | return true
39 | }
40 |
41 | func session(healths []*file.Health, h *sheap.IntHeap) {
42 | for {
43 | if h.Len() == 0 {
44 | logs.Error("health check error")
45 | break
46 | }
47 | rs := heap.Pop(h).(int64) - time.Now().Unix()
48 | if rs <= 0 {
49 | continue
50 | }
51 | timer := time.NewTimer(time.Duration(rs) * time.Second)
52 | select {
53 | case <-timer.C:
54 | for _, v := range healths {
55 | if v.HealthNextTime.Before(time.Now()) {
56 | v.HealthNextTime = time.Now().Add(time.Duration(v.HealthCheckInterval) * time.Second)
57 | //check
58 | go check(v)
59 | //reset time
60 | heap.Push(h, v.HealthNextTime.Unix())
61 | }
62 | }
63 | }
64 | }
65 | }
66 |
67 | // work when just one port and many target
68 | func check(t *file.Health) {
69 | arr := strings.Split(t.HealthCheckTarget, ",")
70 | var err error
71 | var rs *http.Response
72 | for _, v := range arr {
73 | if t.HealthCheckType == "tcp" {
74 | var c net.Conn
75 | c, err = net.DialTimeout("tcp", v, time.Duration(t.HealthCheckTimeout)*time.Second)
76 | if err == nil {
77 | c.Close()
78 | }
79 | } else {
80 | client := &http.Client{}
81 | client.Timeout = time.Duration(t.HealthCheckTimeout) * time.Second
82 | rs, err = client.Get("http://" + v + t.HttpHealthUrl)
83 | if err == nil && rs.StatusCode != 200 {
84 | err = errors.New("status code is not match")
85 | }
86 | }
87 | t.Lock()
88 | if err != nil {
89 | t.HealthMap[v] += 1
90 | } else if t.HealthMap[v] >= t.HealthMaxFail {
91 | //send recovery add
92 | serverConn.SendHealthInfo(v, "1")
93 | t.HealthMap[v] = 0
94 | }
95 |
96 | if t.HealthMap[v] > 0 && t.HealthMap[v]%t.HealthMaxFail == 0 {
97 | //send fail remove
98 | serverConn.SendHealthInfo(v, "0")
99 | }
100 | t.Unlock()
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/client/register.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "encoding/binary"
5 | "log"
6 | "os"
7 |
8 | "ehang.io/nps/lib/common"
9 | )
10 |
11 | func RegisterLocalIp(server string, vKey string, tp string, proxyUrl string, hour int) {
12 | c, err := NewConn(tp, vKey, server, common.WORK_REGISTER, proxyUrl)
13 | if err != nil {
14 | log.Fatalln(err)
15 | }
16 | if err := binary.Write(c, binary.LittleEndian, int32(hour)); err != nil {
17 | log.Fatalln(err)
18 | }
19 | log.Printf("Successful ip registration for local public network, the validity period is %d hours.", hour)
20 | os.Exit(0)
21 | }
22 |
--------------------------------------------------------------------------------
/cmd/npc/sdk.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "C"
5 | "ehang.io/nps/client"
6 | "ehang.io/nps/lib/common"
7 | "ehang.io/nps/lib/version"
8 | "github.com/astaxie/beego/logs"
9 | )
10 |
11 | var cl *client.TRPClient
12 |
13 | //export StartClientByVerifyKey
14 | func StartClientByVerifyKey(serverAddr, verifyKey, connType, proxyUrl *C.char) int {
15 | _ = logs.SetLogger("store")
16 | if cl != nil {
17 | cl.Close()
18 | }
19 | cl = client.NewRPClient(C.GoString(serverAddr), C.GoString(verifyKey), C.GoString(connType), C.GoString(proxyUrl), nil, 60)
20 | cl.Start()
21 | return 1
22 | }
23 |
24 | //export GetClientStatus
25 | func GetClientStatus() int {
26 | return client.NowStatus
27 | }
28 |
29 | //export CloseClient
30 | func CloseClient() {
31 | if cl != nil {
32 | cl.Close()
33 | }
34 | }
35 |
36 | //export Version
37 | func Version() *C.char {
38 | return C.CString(version.VERSION)
39 | }
40 |
41 | //export Logs
42 | func Logs() *C.char {
43 | return C.CString(common.GetLogMsg())
44 | }
45 |
46 | func main() {
47 | // Need a main function to make CGO compile package as C shared library
48 | }
49 |
--------------------------------------------------------------------------------
/cmd/nps/nps.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "log"
6 | "os"
7 | "os/exec"
8 | "path/filepath"
9 | "runtime"
10 | "strings"
11 | "sync"
12 |
13 | "ehang.io/nps/lib/file"
14 | "ehang.io/nps/lib/install"
15 | "ehang.io/nps/lib/version"
16 | "ehang.io/nps/server"
17 | "ehang.io/nps/server/connection"
18 | "ehang.io/nps/server/tool"
19 | "ehang.io/nps/web/routers"
20 |
21 | "ehang.io/nps/lib/common"
22 | "ehang.io/nps/lib/crypt"
23 | "ehang.io/nps/lib/daemon"
24 | "github.com/astaxie/beego"
25 | "github.com/astaxie/beego/logs"
26 |
27 | "github.com/kardianos/service"
28 | )
29 |
30 | var (
31 | level string
32 | ver = flag.Bool("version", false, "show current version")
33 | )
34 |
35 | func main() {
36 | flag.Parse()
37 | // init log
38 | if *ver {
39 | common.PrintVersion()
40 | return
41 | }
42 | if err := beego.LoadAppConfig("ini", filepath.Join(common.GetRunPath(), "conf", "nps.conf")); err != nil {
43 | log.Fatalln("load config file error", err.Error())
44 | }
45 | common.InitPProfFromFile()
46 | if level = beego.AppConfig.String("log_level"); level == "" {
47 | level = "7"
48 | }
49 | logs.Reset()
50 | logs.EnableFuncCallDepth(true)
51 | logs.SetLogFuncCallDepth(3)
52 | logPath := beego.AppConfig.String("log_path")
53 | if logPath == "" {
54 | logPath = common.GetLogPath()
55 | }
56 | if common.IsWindows() {
57 | logPath = strings.Replace(logPath, "\\", "\\\\", -1)
58 | }
59 | // init service
60 | options := make(service.KeyValue)
61 | svcConfig := &service.Config{
62 | Name: "Nps",
63 | DisplayName: "nps内网穿透代理服务器",
64 | Description: "一款轻量级、功能强大的内网穿透代理服务器。支持tcp、udp流量转发,支持内网http代理、内网socks5代理,同时支持snappy压缩、站点保护、加密传输、多路复用、header修改等。支持web图形化管理,集成多用户模式。",
65 | Option: options,
66 | }
67 | svcConfig.Arguments = append(svcConfig.Arguments, "service")
68 | if len(os.Args) > 1 && os.Args[1] == "service" {
69 | _ = logs.SetLogger(logs.AdapterFile, `{"level":`+level+`,"filename":"`+logPath+`","daily":false,"maxlines":100000,"color":true}`)
70 | } else {
71 | _ = logs.SetLogger(logs.AdapterConsole, `{"level":`+level+`,"color":true}`)
72 | }
73 | if !common.IsWindows() {
74 | svcConfig.Dependencies = []string{
75 | "Requires=network.target",
76 | "After=network-online.target syslog.target"}
77 | svcConfig.Option["SystemdScript"] = install.SystemdScript
78 | svcConfig.Option["SysvScript"] = install.SysvScript
79 | }
80 | prg := &nps{}
81 | prg.exit = make(chan struct{})
82 | s, err := service.New(prg, svcConfig)
83 | if err != nil {
84 | logs.Error(err, "service function disabled")
85 | run()
86 | // run without service
87 | wg := sync.WaitGroup{}
88 | wg.Add(1)
89 | wg.Wait()
90 | return
91 | }
92 | if len(os.Args) > 1 && os.Args[1] != "service" {
93 | switch os.Args[1] {
94 | case "reload":
95 | daemon.InitDaemon("nps", common.GetRunPath(), common.GetTmpPath())
96 | return
97 | case "install":
98 | // uninstall before
99 | _ = service.Control(s, "stop")
100 | _ = service.Control(s, "uninstall")
101 |
102 | binPath := install.InstallNps()
103 | svcConfig.Executable = binPath
104 | s, err := service.New(prg, svcConfig)
105 | if err != nil {
106 | logs.Error(err)
107 | return
108 | }
109 | err = service.Control(s, os.Args[1])
110 | if err != nil {
111 | logs.Error("Valid actions: %q\n%s", service.ControlAction, err.Error())
112 | }
113 | if service.Platform() == "unix-systemv" {
114 | logs.Info("unix-systemv service")
115 | confPath := "/etc/init.d/" + svcConfig.Name
116 | os.Symlink(confPath, "/etc/rc.d/S90"+svcConfig.Name)
117 | os.Symlink(confPath, "/etc/rc.d/K02"+svcConfig.Name)
118 | }
119 | return
120 | case "start", "restart", "stop":
121 | if service.Platform() == "unix-systemv" {
122 | logs.Info("unix-systemv service")
123 | cmd := exec.Command("/etc/init.d/"+svcConfig.Name, os.Args[1])
124 | err := cmd.Run()
125 | if err != nil {
126 | logs.Error(err)
127 | }
128 | return
129 | }
130 | err := service.Control(s, os.Args[1])
131 | if err != nil {
132 | logs.Error("Valid actions: %q\n%s", service.ControlAction, err.Error())
133 | }
134 | return
135 | case "uninstall":
136 | err := service.Control(s, os.Args[1])
137 | if err != nil {
138 | logs.Error("Valid actions: %q\n%s", service.ControlAction, err.Error())
139 | }
140 | if service.Platform() == "unix-systemv" {
141 | logs.Info("unix-systemv service")
142 | os.Remove("/etc/rc.d/S90" + svcConfig.Name)
143 | os.Remove("/etc/rc.d/K02" + svcConfig.Name)
144 | }
145 | return
146 | case "update":
147 | install.UpdateNps()
148 | return
149 | default:
150 | logs.Error("command is not support")
151 | return
152 | }
153 | }
154 | _ = s.Run()
155 | }
156 |
157 | type nps struct {
158 | exit chan struct{}
159 | }
160 |
161 | func (p *nps) Start(s service.Service) error {
162 | _, _ = s.Status()
163 | go p.run()
164 | return nil
165 | }
166 | func (p *nps) Stop(s service.Service) error {
167 | _, _ = s.Status()
168 | close(p.exit)
169 | if service.Interactive() {
170 | os.Exit(0)
171 | }
172 | return nil
173 | }
174 |
175 | func (p *nps) run() error {
176 | defer func() {
177 | if err := recover(); err != nil {
178 | const size = 64 << 10
179 | buf := make([]byte, size)
180 | buf = buf[:runtime.Stack(buf, false)]
181 | logs.Warning("nps: panic serving %v: %v\n%s", err, string(buf))
182 | }
183 | }()
184 | run()
185 | select {
186 | case <-p.exit:
187 | logs.Warning("stop...")
188 | }
189 | return nil
190 | }
191 |
192 | func run() {
193 | routers.Init()
194 | task := &file.Tunnel{
195 | Mode: "webServer",
196 | }
197 | bridgePort, err := beego.AppConfig.Int("bridge_port")
198 | if err != nil {
199 | logs.Error("Getting bridge_port error", err)
200 | os.Exit(0)
201 | }
202 | logs.Info("the version of server is %s ,allow client core version to be %s", version.VERSION, version.GetVersion())
203 | connection.InitConnectionService()
204 | //crypt.InitTls(filepath.Join(common.GetRunPath(), "conf", "server.pem"), filepath.Join(common.GetRunPath(), "conf", "server.key"))
205 | crypt.InitTls()
206 | tool.InitAllowPort()
207 | tool.StartSystemInfo()
208 | timeout, err := beego.AppConfig.Int("disconnect_timeout")
209 | if err != nil {
210 | timeout = 60
211 | }
212 | go server.StartNewServer(bridgePort, task, beego.AppConfig.String("bridge_type"), timeout)
213 | }
214 |
--------------------------------------------------------------------------------
/conf/clients.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/conf/clients.json
--------------------------------------------------------------------------------
/conf/hosts.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/conf/hosts.json
--------------------------------------------------------------------------------
/conf/multi_account.conf:
--------------------------------------------------------------------------------
1 | # key -> user | value -> pwd
2 | npc=npc.pwd
--------------------------------------------------------------------------------
/conf/npc.conf:
--------------------------------------------------------------------------------
1 | [common]
2 | server_addr=127.0.0.1:8024
3 | conn_type=tcp
4 | vkey=123
5 | auto_reconnection=true
6 | max_conn=1000
7 | flow_limit=1000
8 | rate_limit=1000
9 | basic_username=11
10 | basic_password=3
11 | web_username=user
12 | web_password=1234
13 | crypt=true
14 | compress=true
15 | #pprof_addr=0.0.0.0:9999
16 | disconnect_timeout=60
17 |
18 | [health_check_test1]
19 | health_check_timeout=1
20 | health_check_max_failed=3
21 | health_check_interval=1
22 | health_http_url=/
23 | health_check_type=http
24 | health_check_target=127.0.0.1:8083,127.0.0.1:8082
25 |
26 | [health_check_test2]
27 | health_check_timeout=1
28 | health_check_max_failed=3
29 | health_check_interval=1
30 | health_check_type=tcp
31 | health_check_target=127.0.0.1:8083,127.0.0.1:8082
32 |
33 | [web]
34 | host=c.o.com
35 | target_addr=127.0.0.1:8083,127.0.0.1:8082
36 |
37 | [tcp]
38 | mode=tcp
39 | target_addr=127.0.0.1:8080
40 | server_port=10000
41 |
42 | [socks5]
43 | mode=socks5
44 | server_port=19009
45 | multi_account=multi_account.conf
46 |
47 | [file]
48 | mode=file
49 | server_port=19008
50 | local_path=/Users/liuhe/Downloads
51 | strip_pre=/web/
52 |
53 | [http]
54 | mode=httpProxy
55 | server_port=19004
56 |
57 | [udp]
58 | mode=udp
59 | server_port=12253
60 | target_addr=114.114.114.114:53
61 |
62 | [ssh_secret]
63 | mode=secret
64 | password=ssh2
65 | target_addr=123.206.77.88:22
66 |
67 | [ssh_p2p]
68 | mode=p2p
69 | password=ssh3
70 |
71 | [secret_ssh]
72 | local_port=2001
73 | password=ssh2
74 |
75 | [p2p_ssh]
76 | local_port=2002
77 | password=ssh3
78 | target_addr=123.206.77.88:22
--------------------------------------------------------------------------------
/conf/nps.conf:
--------------------------------------------------------------------------------
1 | appname = nps
2 | #Boot mode(dev|pro)
3 | runmode = dev
4 |
5 | #HTTP(S) proxy port, no startup if empty
6 | http_proxy_ip=0.0.0.0
7 | http_proxy_port=80
8 | https_proxy_port=443
9 | https_just_proxy=true
10 | #default https certificate setting
11 | https_default_cert_file=conf/server.pem
12 | https_default_key_file=conf/server.key
13 |
14 | ##bridge
15 | bridge_type=tcp
16 | bridge_port=8024
17 | bridge_ip=0.0.0.0
18 |
19 | # Public password, which clients can use to connect to the server
20 | # After the connection, the server will be able to open relevant ports and parse related domain names according to its own configuration file.
21 | public_vkey=123
22 |
23 | #Traffic data persistence interval(minute)
24 | #Ignorance means no persistence
25 | #flow_store_interval=1
26 |
27 | # log level LevelEmergency->0 LevelAlert->1 LevelCritical->2 LevelError->3 LevelWarning->4 LevelNotice->5 LevelInformational->6 LevelDebug->7
28 | log_level=7
29 | #log_path=nps.log
30 |
31 | #Whether to restrict IP access, true or false or ignore
32 | #ip_limit=true
33 |
34 | #p2p
35 | #p2p_ip=127.0.0.1
36 | #p2p_port=6000
37 |
38 | #web
39 | web_host=a.o.com
40 | web_username=admin
41 | web_password=123
42 | web_port = 8080
43 | web_ip=0.0.0.0
44 | web_base_url=
45 | web_open_ssl=false
46 | web_cert_file=conf/server.pem
47 | web_key_file=conf/server.key
48 | # if web under proxy use sub path. like http://host/nps need this.
49 | #web_base_url=/nps
50 |
51 | #Web API unauthenticated IP address(the len of auth_crypt_key must be 16)
52 | #Remove comments if needed
53 | #auth_key=test
54 | auth_crypt_key =1234567812345678
55 |
56 | #allow_ports=9001-9009,10001,11000-12000
57 |
58 | #Web management multi-user login
59 | allow_user_login=false
60 | allow_user_register=false
61 | allow_user_change_username=false
62 |
63 |
64 | #extension
65 | allow_flow_limit=false
66 | allow_rate_limit=false
67 | allow_tunnel_num_limit=false
68 | allow_local_proxy=false
69 | allow_connection_num_limit=false
70 | allow_multi_ip=false
71 | system_info_display=false
72 |
73 | #cache
74 | http_cache=false
75 | http_cache_length=100
76 |
77 | #get origin ip
78 | http_add_origin_header=false
79 |
80 | #pprof debug options
81 | #pprof_ip=0.0.0.0
82 | #pprof_port=9999
83 |
84 | #client disconnect timeout
85 | disconnect_timeout=60
86 |
--------------------------------------------------------------------------------
/conf/server.key:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEpAIBAAKCAQEA2MVLOHvgU8FCp6LgQrPfaWcGygrsRk7TL9hbT8MxbCRUSLV7
3 | Lbt3q5Knz8eTN4NWmwE6L5glOcH2x3Hnn+hPjbvgq35XBBIccAm0cYYKqoKkikeK
4 | FZM0Gp/WhSrhJ4laTyQqyleIFKpwD9kHDiC/sxjGDhSFmHKhhAnsQIRm2tppFXX0
5 | aAMqJEm88jzk1BN2QtKjEAn1u8v1+QW1KP3WuzdXH4L7hhMll66/KIm6Hfs2FRHQ
6 | pRUWqZeJY4q79NW5p5f+siGwOsGpxb/p11pM+0xnCH3UIFbm3zCTzP4sLvkfFGAe
7 | yAHsAwmaP8dJxh40ej3NN8uNiNvt8nw2Vb/1LwIDAQABAoIBAD40x/RKoEKIyE8B
8 | D6g0pB1EQo+CePFoN3SYewO1uR4WgtVmtxWVoa7r5BpdZGLe3uCWhpMX7z7W6bGs
9 | f1LFQOckjkHIfMIfTGfecRjO5Yqu+Pbxtq+gUah+S/plJr3IzdC+SUVNvzBnBMeX
10 | eU3Vmg2UQ2nQ+9GWu8D/c/vDwxx0X8oQ2G8QaxX0tUurlSMNA3M7xySwEvhx54fO
11 | UrDF3Q4yF48eA4butxVLFWf3cnlY+nR8uYd2vKfmp689/8C6kkfoM9igB78e93sm
12 | uDM2eRLm4kU5WLl301T42n6AF7w8J0MhLLVOIeLs4l5gZPa3uKvYFmuHQao7e/5R
13 | U/jHKrECgYEA8alPXuxFSVOvdhIsSN//Frj9CdExVdYmaLkt/2LO4FMnOaWh1xh7
14 | 5iCY1bJT8D9dhfbqRg3qW2oguZD8gu04R8fTRegQ89qmAIwsEYqVf9salR41lZU4
15 | Rc+5yc7O11WIe9Lzu+ONFBFkAh3UFMR4zVZ/JhKIG/P5Srm7SUdKW2cCgYEA5aHo
16 | x2LR+yKhjkrBzHG3Qrfy1PtlYHjOpYYAKHQcBFuiG08W3CK/vkYl+mhv0uyhT7mn
17 | q6NDqrpZPRnDlOoEqgRS1X/QWKN6Pgd4HNLIawvp0vK9jYXDPcAXFzVthXCIwFcn
18 | 3a3m4cHiuLdRNOHkydiHQyTOF6eEneN07TDvwvkCgYEApzOd1u9igPmFzQuF2GYi
19 | +HXFnaU/nUQuDwcQ7EJRIKRn31raPxiRoQesty5LJU6yRp4wOYgnPliPi9Tk4TGA
20 | XynC4/tMv2vorzhMxVY9Wdke602bhYNZC/RNd3O/aP2lEQdD3Bv04I2nxE8fDb9i
21 | VbAjCRSJV83WDf2zt1+78sECgYEAzezjRiKdcZu9y0/I+WEk2cUCE/MaF2he0FsZ
22 | uy1cjp/qAJltQ5452xUnK6cKWNlxU4CHF0mC/hC8xCldliZCZoEYE3PaUBLSJdwm
23 | 35o6tpxpZI3gZJCG5NJlIp/8BkVDrVC7ZHV17hAkFEf4n/bPaB8wNYtE8jt8luaK
24 | TcarzGkCgYBn2alN0RLN2PHDurraFZB6GuCvh/arEjSCY3SDFQPF10CVjTDV7sx3
25 | eqJkwJ81syTmfJwZIceWbOFGgsuSx37UrQAVlHZSvzeqEg9dA5HqSoOACyidJI7j
26 | RG2+HB+KpsIZjGgLrEM4i7VOpYUDRdaouIXngFq/t9HNT+MDck5/Lw==
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/conf/server.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDtTCCAp2gAwIBAgIJAPXRSiP0Fs7sMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
3 | BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
4 | aWRnaXRzIFB0eSBMdGQwHhcNMTcxMTA3MDg1MzQ2WhcNMjcxMTA1MDg1MzQ2WjBF
5 | MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
6 | ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
7 | CgKCAQEA2MVLOHvgU8FCp6LgQrPfaWcGygrsRk7TL9hbT8MxbCRUSLV7Lbt3q5Kn
8 | z8eTN4NWmwE6L5glOcH2x3Hnn+hPjbvgq35XBBIccAm0cYYKqoKkikeKFZM0Gp/W
9 | hSrhJ4laTyQqyleIFKpwD9kHDiC/sxjGDhSFmHKhhAnsQIRm2tppFXX0aAMqJEm8
10 | 8jzk1BN2QtKjEAn1u8v1+QW1KP3WuzdXH4L7hhMll66/KIm6Hfs2FRHQpRUWqZeJ
11 | Y4q79NW5p5f+siGwOsGpxb/p11pM+0xnCH3UIFbm3zCTzP4sLvkfFGAeyAHsAwma
12 | P8dJxh40ej3NN8uNiNvt8nw2Vb/1LwIDAQABo4GnMIGkMB0GA1UdDgQWBBQdPc0R
13 | a8alY6Ab7voidkTGaH4PxzB1BgNVHSMEbjBsgBQdPc0Ra8alY6Ab7voidkTGaH4P
14 | x6FJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV
15 | BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAPXRSiP0Fs7sMAwGA1UdEwQF
16 | MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAH1IZNkjuvt2nZPzXsuiVNyCE1vm346z
17 | naE0Uzt3aseAN9m/iiB8mLz+ryvWc2aFMX5lTdsHdm2rqmqBCBXeRwTLf4OeHIju
18 | ZQW6makWt6PxANEo6gbdPbQXbS420ssUhnR2irIH1SdI31iikVFPdiS0baRRE/gS
19 | +440M1jOOOnKm0Qin92ejsshmji/0qaD2+6D5TNw4HmIZaFTBw+kfjxCL6trfeBn
20 | 4fT0RJ121V3G3+AtG5sWQ93B3pCg+jtD+fGKkNSLhphq84bD1Zv7l73QGOoylkEn
21 | Sc0ajTLOXFBb83yRdlgV3Da95jH9rDZ4jSod48m+KemoZTDQw0vSwAU=
22 | -----END CERTIFICATE-----
23 |
--------------------------------------------------------------------------------
/conf/tasks.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/conf/tasks.json
--------------------------------------------------------------------------------
/docs/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/docs/.nojekyll
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # nps
2 |  
3 | [](https://gitter.im/cnlh-nps/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
4 | [](https://travis-ci.org/cnlh/nps)
5 |
6 | nps是一款轻量级、高性能、功能强大的**内网穿透**代理服务器。目前支持**tcp、udp流量转发**,可支持任何**tcp、udp**上层协议(访问内网网站、本地支付接口调试、ssh访问、远程桌面,内网dns解析等等……),此外还**支持内网http代理、内网socks5代理**、**p2p等**,并带有功能强大的web管理端。
7 |
8 |
9 | ## 背景
10 | 
11 |
12 | 1. 做微信公众号开发、小程序开发等----> 域名代理模式
13 |
14 |
15 | 2. 想在外网通过ssh连接内网的机器,做云服务器到内网服务器端口的映射,----> tcp代理模式
16 |
17 | 3. 在非内网环境下使用内网dns,或者需要通过udp访问内网机器等----> udp代理模式
18 |
19 | 4. 在外网使用HTTP代理访问内网站点----> http代理模式
20 |
21 | 5. 搭建一个内网穿透ss,在外网如同使用内网vpn一样访问内网资源或者设备----> socks5代理模式
22 |
--------------------------------------------------------------------------------
/docs/_coverpage.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # NPS 0.26.10
4 |
5 | > 一款轻量级、高性能、功能强大的内网穿透代理服务器
6 |
7 | - 几乎支持所有协议
8 | - 支持内网http代理、内网socks5代理、p2p等
9 | - 简洁但功能强大的WEB管理界面
10 | - 支持服务端、客户端同时控制
11 | - 扩展功能强大
12 | - 全平台兼容,一键注册为服务
13 |
14 |
15 | [GitHub](https://github.com/ehang-io/nps/)
16 | [开始使用](#nps)
17 |
--------------------------------------------------------------------------------
/docs/_navbar.md:
--------------------------------------------------------------------------------
1 | * [](https://github.com/ehang-io/nps/stargazers)
2 |
3 | * [](https://github.com/ehang-io/nps/network)
--------------------------------------------------------------------------------
/docs/_sidebar.md:
--------------------------------------------------------------------------------
1 | * 入门
2 | * [安装](install.md)
3 | * [启动](run.md)
4 | * [使用示例](example.md)
5 | * 服务端
6 | * [介绍](introduction.md)
7 | * [使用](nps_use.md)
8 | * [配置文件](server_config.md)
9 | * [增强功能](nps_extend.md)
10 |
11 | * 客户端
12 |
13 | * [基本使用](use.md)
14 | * [增强功能](npc_extend.md)
15 |
16 | * 扩展
17 |
18 | * [功能](feature.md)
19 | * [说明](description.md)
20 | * [web api](api.md)
21 | * [sdk](npc_sdk.md)
22 |
23 | * 其他
24 |
25 | * [FAQ](faq.md)
26 | * [贡献](contribute.md)
27 | * [捐助](donate.md)
28 | * [致谢](thanks.md)
29 | * [交流](discuss.md)
30 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | # web api
2 |
3 | 需要开启请先去掉`nps.conf`中`auth_key`的注释并配置一个合适的密钥
4 | ## webAPI验证说明
5 | - 采用auth_key的验证方式
6 | - 在提交的每个请求后面附带两个参数,`auth_key` 和`timestamp`
7 |
8 | ```
9 | auth_key的生成方式为:md5(配置文件中的auth_key+当前时间戳)
10 | ```
11 |
12 | ```
13 | timestamp为当前时间戳
14 | ```
15 | ```
16 | curl --request POST \
17 | --url http://127.0.0.1:8080/client/list \
18 | --data 'auth_key=2a0000d9229e7dbcf79dd0f5e04bb084×tamp=1553045344&start=0&limit=10'
19 | ```
20 | **注意:** 为保证安全,时间戳的有效范围为20秒内,所以每次提交请求必须重新生成。
21 |
22 | ## 获取服务端时间
23 | 由于服务端与api请求的客户端时间差异不能太大,所以提供了一个可以获取服务端时间的接口
24 |
25 | ```
26 | POST /auth/gettime
27 | ```
28 |
29 | ## 获取服务端authKey
30 |
31 | 如果想获取authKey,服务端提供获取authKey的接口
32 |
33 | ```
34 | POST /auth/getauthkey
35 | ```
36 | 将返回加密后的authKey,采用aes cbc加密,请使用与服务端配置文件中cryptKey相同的密钥进行解密
37 |
38 | **注意:** nps配置文件中`auth_crypt_key`需为16位
39 | - 解密密钥长度128
40 | - 偏移量与密钥相同
41 | - 补码方式pkcs5padding
42 | - 解密串编码方式 十六进制
43 |
44 | ## 详细文档
45 | - **[详见](webapi.md)** (感谢@avengexyz)
46 |
--------------------------------------------------------------------------------
/docs/contribute.md:
--------------------------------------------------------------------------------
1 | # 贡献
2 |
3 | - 如果遇到bug可以直接提交至dev分支
4 | - 使用遇到问题可以通过issues反馈
5 | - 项目处于开发阶段,还有很多待完善的地方,如果可以贡献代码,请提交 PR 至 dev 分支
6 | - 如果有新的功能特性反馈,可以通过issues或者qq群反馈
7 |
--------------------------------------------------------------------------------
/docs/description.md:
--------------------------------------------------------------------------------
1 | # 说明
2 | ## 获取用户真实ip
3 | 如需使用需要在`nps.conf`中设置`http_add_origin_header=true`
4 |
5 | 在域名代理模式中,可以通过request请求 header 中的 X-Forwarded-For 和 X-Real-IP 来获取用户真实 IP。
6 |
7 | **本代理前会在每一个http(s)请求中添加了这两个 header。**
8 |
9 | ## 热更新支持
10 | 对于绝大多数配置,在web管理中的修改将实时使用,无需重启客户端或者服务端
11 |
12 | ## 客户端地址显示
13 | 在web管理中将显示客户端的连接地址
14 |
15 | ## 流量统计
16 | 可统计显示每个代理使用的流量,由于压缩和加密等原因,会和实际环境中的略有差异
17 |
18 | ## 当前客户端带宽
19 | 可统计每个客户端当前的带宽,可能和实际有一定差异,仅供参考。
20 |
21 | ## 客户端与服务端版本对比
22 | 为了程序正常运行,客户端与服务端的核心版本必须一致,否则将导致客户端无法成功连接致服务端。
23 |
24 | ## Linux系统限制
25 | 默认情况下linux对连接数量有限制,对于性能好的机器完全可以调整内核参数以处理更多的连接。
26 | `tcp_max_syn_backlog` `somaxconn`
27 | 酌情调整参数,增强网络性能
28 |
29 | ## web管理保护
30 | 当一个ip连续登陆失败次数超过10次,将在一分钟内禁止该ip再次尝试。
31 |
--------------------------------------------------------------------------------
/docs/discuss.md:
--------------------------------------------------------------------------------
1 | # 交流群
2 |
3 | 
4 |
--------------------------------------------------------------------------------
/docs/donate.md:
--------------------------------------------------------------------------------
1 | # 捐助
2 | 如果您觉得nps对你有帮助,欢迎给予我们一定捐助,也是帮助nps更好的发展。
3 |
4 | ## 支付宝
5 | 
6 | ## 微信
7 | 
8 |
--------------------------------------------------------------------------------
/docs/example.md:
--------------------------------------------------------------------------------
1 | # 使用示例
2 | ## 统一准备工作(必做)
3 | - 开启服务端,假设公网服务器ip为1.1.1.1,配置文件中`bridge_port`为8024,配置文件中`web_port`为8080
4 | - 访问1.1.1.1:8080
5 | - 在客户端管理中创建一个客户端,记录下验证密钥
6 | - 内网客户端运行(windows使用cmd运行加.exe)
7 |
8 | ```shell
9 | ./npc -server=1.1.1.1:8024 -vkey=客户端的密钥
10 | ```
11 | **注意:运行服务端后,请确保能从客户端设备上正常访问配置文件中所配置的`bridge_port`端口,telnet,netcat这类的来检查**
12 |
13 | ## 域名解析
14 |
15 | **适用范围:** 小程序开发、微信公众号开发、产品演示
16 |
17 | **注意:域名解析模式为http反向代理,不是dns服务器,在web上能够轻松灵活配置**
18 |
19 | **假设场景:**
20 | - 有一个域名proxy.com,有一台公网机器ip为1.1.1.1
21 | - 两个内网开发站点127.0.0.1:81,127.0.0.1:82
22 | - 想通过(http|https://)a.proxy.com访问127.0.0.1:81,通过(http|https://)b.proxy.com访问127.0.0.1:82
23 |
24 | **使用步骤**
25 | - 将*.proxy.com解析到公网服务器1.1.1.1
26 | - 点击刚才创建的客户端的域名管理,添加两条规则规则:1、域名:`a.proxy.com`,内网目标:`127.0.0.1:81`,2、域名:`b.proxy.com`,内网目标:`127.0.0.1:82`
27 |
28 | 现在访问(http|https://)`a.proxy.com`,`b.proxy.com`即可成功
29 |
30 | **https:** 如需使用https请进行相关配置,详见 [使用https](/nps_extend?id=使用https)
31 |
32 | ## tcp隧道
33 |
34 |
35 | **适用范围:** ssh、远程桌面等tcp连接场景
36 |
37 | **假设场景:**
38 | 想通过访问公网服务器1.1.1.1的8001端口,连接内网机器10.1.50.101的22端口,实现ssh连接
39 |
40 | **使用步骤**
41 | - 在刚才创建的客户端隧道管理中添加一条tcp隧道,填写监听的端口(8001)、内网目标ip和目标端口(10.1.50.101:22),保存。
42 | - 访问公网服务器ip(1.1.1.1),填写的监听端口(8001),相当于访问内网ip(10.1.50.101):目标端口(22),例如:`ssh -p 8001 root@1.1.1.1`
43 |
44 | ## udp隧道
45 |
46 | **适用范围:** 内网dns解析等udp连接场景
47 |
48 | **假设场景:**
49 | 内网有一台dns(10.1.50.102:53),在非内网环境下想使用该dns,公网服务器为1.1.1.1
50 |
51 | **使用步骤**
52 | - 在刚才创建的客户端的隧道管理中添加一条udp隧道,填写监听的端口(53)、内网目标ip和目标端口(10.1.50.102:53),保存。
53 | - 修改需要使用的dns地址为1.1.1.1,则相当于使用10.1.50.102作为dns服务器
54 |
55 | ## socks5代理
56 |
57 |
58 | **适用范围:** 在外网环境下如同使用vpn一样访问内网设备或者资源
59 |
60 | **假设场景:**
61 | 想将公网服务器1.1.1.1的8003端口作为socks5代理,达到访问内网任意设备或者资源的效果
62 |
63 | **使用步骤**
64 | - 在刚才创建的客户端隧道管理中添加一条socks5代理,填写监听的端口(8003),保存。
65 | - 在外网环境的本机配置socks5代理(例如使用proxifier进行全局代理),ip为公网服务器ip(1.1.1.1),端口为填写的监听端口(8003),即可畅享内网了
66 |
67 | **注意**
68 | 经过socks5代理,当收到socks5数据包时socket已经是accept状态。表现是扫描端口全open,建立连接后短时间关闭。若想同内网表现一致,建议远程连接一台设备。
69 |
70 | ## http正向代理
71 |
72 | **适用范围:** 在外网环境下使用http正向代理访问内网站点
73 |
74 | **假设场景:**
75 | 想将公网服务器1.1.1.1的8004端口作为http代理,访问内网网站
76 |
77 | **使用步骤**
78 |
79 | - 在刚才创建的客户端隧道管理中添加一条http代理,填写监听的端口(8004),保存。
80 | - 在外网环境的本机配置http代理,ip为公网服务器ip(1.1.1.1),端口为填写的监听端口(8004),即可访问了
81 |
82 | **注意:对于私密代理与p2p,除了统一配置的客户端和服务端,还需要一个客户端作为访问端提供一个端口来访问**
83 |
84 | ## 私密代理
85 |
86 | **适用范围:** 无需占用多余的端口、安全性要求较高可以防止其他人连接的tcp服务,例如ssh。
87 |
88 | **假设场景:**
89 | 无需新增多的端口实现访问内网服务器10.1.50.2的22端口
90 |
91 | **使用步骤**
92 | - 在刚才创建的客户端中添加一条私密代理,并设置唯一密钥secrettest和内网目标10.1.50.2:22
93 | - 在需要连接ssh的机器上以执行命令
94 |
95 | ```
96 | ./npc -server=1.1.1.1:8024 -vkey=vkey -type=tcp -password=secrettest -local_type=secret
97 | ```
98 | 如需指定本地端口可加参数`-local_port=xx`,默认为2000
99 |
100 | **注意:** password为web管理上添加的唯一密钥,具体命令可查看web管理上的命令提示
101 |
102 | 假设10.1.50.2用户名为root,现在执行`ssh -p 2000 root@127.0.0.1`即可访问ssh
103 |
104 |
105 | ## p2p服务
106 |
107 | **适用范围:** 大流量传输场景,流量不经过公网服务器,但是由于p2p穿透和nat类型关系较大,不保证100%成功,支持大部分nat类型。[nat类型检测](/npc_extend?id=nat类型检测)
108 |
109 | **假设场景:**
110 |
111 | 想通过访问使用端机器(访问端,也就是本机)的2000端口---->访问到内网机器 10.2.50.2的22端口
112 |
113 | **使用步骤**
114 | - 在`nps.conf`中设置`p2p_ip`(nps服务器ip)和`p2p_port`(nps服务器udp端口)
115 | > 注:若 `p2p_port` 设置为6000,请在防火墙开放6000~6002(额外添加2个端口)udp端口
116 | - 在刚才刚才创建的客户端中添加一条p2p代理,并设置唯一密钥p2pssh
117 | - 在使用端机器(本机)执行命令
118 |
119 | ```
120 | ./npc -server=1.1.1.1:8024 -vkey=123 -password=p2pssh -target=10.2.50.2:22
121 | ```
122 | 如需指定本地端口可加参数`-local_port=xx`,默认为2000
123 |
124 | **注意:** password为web管理上添加的唯一密钥,具体命令可查看web管理上的命令提示
125 |
126 | 假设内网机器为10.2.50.2的ssh用户名为root,现在在本机上执行`ssh -p 2000 root@127.0.0.1`即可访问机器2的ssh,如果是网站在浏览器访问127.0.0.1:2000端口即可。
127 |
--------------------------------------------------------------------------------
/docs/faq.md:
--------------------------------------------------------------------------------
1 | # FAQ
2 |
3 | - 服务端无法启动
4 | ```
5 | 服务端默认配置启用了8024,8080,80,443端口,端口冲突无法启动,请修改配置
6 | ```
7 | - 客户端无法连接服务端
8 | ```
9 | 请检查配置文件中的所有端口是否在安全组,防火墙放行
10 | 请检查vkey是否对应
11 | 请检查版本是否对应
12 | ```
13 | - 服务端配置文件修改无效
14 | ```
15 | install 之后,Linux 配置文件在 /etc/nps
16 | ```
17 | - p2p穿透失败 [p2p服务](https://ehang-io.github.io/nps/#/example?id=p2p%e6%9c%8d%e5%8a%a1)
18 | ```
19 | 双方nat类型都是Symmetric Nat一定不成功,建议先查看nat类型。请按照文档操作(标题上有超链接)
20 | ```
21 |
--------------------------------------------------------------------------------
/docs/feature.md:
--------------------------------------------------------------------------------
1 | # 扩展功能
2 | ## 缓存支持
3 | 对于web站点来说,一些静态文件往往消耗更大的流量,且在内网穿透中,静态文件还需到客户端获取一次,这将导致更大的流量消耗。nps在域名解析代理中支持对静态文件进行缓存。
4 |
5 | 即假设一个站点有a.css,nps将只需从npc客户端读取一次该文件,然后把该文件的内容放在内存中,下一次将不再对npc客户端进行请求而直接返回内存中的对应内容。该功能默认是关闭的,如需开启请在`nps.conf`中设置`http_cache=true`,并设置`http_cache_length`(缓存文件的个数,消耗内存,不宜过大,0表示不限制个数)
6 |
7 | ## 数据压缩支持
8 |
9 | 由于是内网穿透,内网客户端与服务端之间的隧道存在大量的数据交换,为节省流量,加快传输速度,由此本程序支持SNNAPY形式的压缩。
10 |
11 |
12 | - 所有模式均支持数据压缩
13 | - 在web管理或客户端配置文件中设置
14 |
15 |
16 | ## 加密传输
17 |
18 | 如果公司内网防火墙对外网访问进行了流量识别与屏蔽,例如禁止了ssh协议等,通过设置 配置文件,将服务端与客户端之间的通信内容加密传输,将会有效防止流量被拦截。
19 | - nps现在默认每次启动时随机生成tls证书,用于加密传输
20 |
21 |
22 |
23 | ## 站点保护
24 | 域名代理模式所有客户端共用一个http服务端口,在知道域名后任何人都可访问,一些开发或者测试环境需要保密,所以可以设置用户名和密码,nps将通过 Http Basic Auth 来保护,访问时需要输入正确的用户名和密码。
25 |
26 |
27 | - 在web管理或客户端配置文件中设置
28 |
29 | ## host修改
30 |
31 | 由于内网站点需要的host可能与公网域名不一致,域名代理支持host修改功能,即修改request的header中的host字段。
32 |
33 | **使用方法:在web管理中设置**
34 |
35 | ## 自定义header
36 |
37 | 支持对header进行新增或者修改,以配合服务的需要
38 |
39 | ## 404页面配置
40 | 支持域名解析模式的自定义404页面,修改/web/static/page/error.html中内容即可,暂不支持静态文件等内容
41 |
42 | ## 流量限制
43 |
44 | 支持客户端级流量限制,当该客户端入口流量与出口流量达到设定的总量后会拒绝服务
45 | ,域名代理会返回404页面,其他代理会拒绝连接,使用该功能需要在`nps.conf`中设置`allow_flow_limit`,默认是关闭的。
46 |
47 | ## 带宽限制
48 |
49 | 支持客户端级带宽限制,带宽计算方式为入口和出口总和,权重均衡,使用该功能需要在`nps.conf`中设置`allow_rate_limit`,默认是关闭的。
50 |
51 | ## 负载均衡
52 | 本代理支持域名解析模式和tcp代理的负载均衡,在web域名添加或者编辑中内网目标分行填写多个目标即可实现轮训级别的负载均衡
53 |
54 | ## 端口白名单
55 | 为了防止服务端上的端口被滥用,可在nps.conf中配置allow_ports限制可开启的端口,忽略或者不填表示端口不受限制,格式:
56 |
57 | ```ini
58 | allow_ports=9001-9009,10001,11000-12000
59 | ```
60 |
61 | ## 端口范围映射
62 | 当客户端以配置文件的方式启动时,可以将本地的端口进行范围映射,仅支持tcp和udp模式,例如:
63 |
64 | ```ini
65 | [tcp]
66 | mode=tcp
67 | server_port=9001-9009,10001,11000-12000
68 | target_port=8001-8009,10002,13000-14000
69 | ```
70 |
71 | 逗号分隔,可单个或者范围,注意上下端口的对应关系,无法一一对应将不能成功
72 | ## 端口范围映射到其他机器
73 | ```ini
74 | [tcp]
75 | mode=tcp
76 | server_port=9001-9009,10001,11000-12000
77 | target_port=8001-8009,10002,13000-14000
78 | target_ip=10.1.50.2
79 | ```
80 | 填写target_ip后则表示映射的该地址机器的端口,忽略则便是映射本地127.0.0.1,仅范围映射时有效
81 |
82 | ## KCP协议支持
83 |
84 | 在网络质量非常好的情况下,例如专线,内网,可以开启略微降低延迟。如需使用可在nps.conf中修改`bridge_type`为kcp
85 | ,设置后本代理将开启udp端口(`bridge_port`)
86 |
87 | 注意:当服务端为kcp时,客户端连接时也需要使用相同配置,无配置文件模式加上参数type=kcp,配置文件模式在配置文件中设置tp=kcp
88 |
89 | ## 域名泛解析
90 | 支持域名泛解析,例如将host设置为*.proxy.com,a.proxy.com、b.proxy.com等都将解析到同一目标,在web管理中或客户端配置文件中将host设置为此格式即可。
91 |
92 | ## URL路由
93 | 本代理支持根据URL将同一域名转发到不同的内网服务器,可在web中或客户端配置文件中设置,此参数也可忽略,例如在客户端配置文件中
94 |
95 | ```ini
96 | [web1]
97 | host=a.proxy.com
98 | target_addr=127.0.0.1:7001
99 | location=/test
100 | [web2]
101 | host=a.proxy.com
102 | target_addr=127.0.0.1:7002
103 | location=/static
104 | ```
105 | 对于`a.proxy.com/test`将转发到`web1`,对于`a.proxy.com/static`将转发到`web2`
106 |
107 | ## 限制ip访问
108 | 如果将一些危险性高的端口例如ssh端口暴露在公网上,可能会带来一些风险,本代理支持限制ip访问。
109 |
110 | **使用方法:** 在配置文件nps.conf中设置`ip_limit`=true,设置后仅通过注册的ip方可访问。
111 |
112 | **ip注册**:
113 |
114 | **方式一:**
115 | 在需要访问的机器上,运行客户端
116 |
117 | ```
118 | ./npc register -server=ip:port -vkey=公钥或客户端密钥 time=2
119 | ```
120 |
121 | time为有效小时数,例如time=2,在当前时间后的两小时内,本机公网ip都可以访问nps代理.
122 |
123 | **方式二:**
124 | 此外nps的web登陆也可提供验证的功能,成功登陆nps web admin后将自动为登陆的ip注册两小时的允许访问权限。
125 |
126 |
127 | **注意:** 本机公网ip并不是一成不变的,请自行注意有效期的设置,同时同一网络下,多人也可能是在公用同一个公网ip。
128 | ## 客户端最大连接数
129 | 为防止恶意大量长连接,影响服务端程序的稳定性,可以在web或客户端配置文件中为每个客户端设置最大连接数。该功能针对`socks5`、`http正向代理`、`域名代理`、`tcp代理`、`udp代理`、`私密代理`生效,使用该功能需要在`nps.conf`中设置`allow_connection_num_limit=true`,默认是关闭的。
130 |
131 | ## 客户端最大隧道数限制
132 | nps支持对客户端的隧道数量进行限制,该功能默认是关闭的,如需开启,请在`nps.conf`中设置`allow_tunnel_num_limit=true`。
133 | ## 端口复用
134 | 在一些严格的网络环境中,对端口的个数等限制较大,nps支持强大端口复用功能。将`bridge_port`、 `http_proxy_port`、 `https_proxy_port` 、`web_port`都设置为同一端口,也能正常使用。
135 |
136 | - 使用时将需要复用的端口设置为与`bridge_port`一致即可,将自动识别。
137 | - 如需将web管理的端口也复用,需要配置`web_host`也就是一个二级域名以便区分
138 |
139 | ## 多路复用
140 |
141 | nps主要通信默认基于多路复用,无需开启。
142 |
143 | 多路复用基于TCP滑动窗口原理设计,动态计算延迟以及带宽来算出应该往网络管道中打入的流量。
144 | 由于主要通信大多采用TCP协议,并无法探测其实时丢包情况,对于产生丢包重传的情况,采用较大的宽容度,
145 | 5分钟的等待时间,超时将会关闭当前隧道连接并重新建立,这将会抛弃当前所有的连接。
146 | 在Linux上,可以通过调节内核参数来适应不同应用场景。
147 |
148 | 对于需求大带宽又有一定的丢包的场景,可以保持默认参数不变,尽可能少抛弃连接
149 | 高并发下可根据[Linux系统限制](## Linux系统限制) 调整
150 |
151 | 对于延迟敏感而又有一定丢包的场景,可以适当调整TCP重传次数
152 | `tcp_syn_retries`, `tcp_retries1`, `tcp_retries2`
153 | 高并发同上
154 | nps会在系统主动关闭连接的时候拿到报错,进而重新建立隧道连接
155 |
156 | ## 环境变量渲染
157 | npc支持环境变量渲染以适应在某些特殊场景下的要求。
158 |
159 | **在无配置文件启动模式下:**
160 | 设置环境变量
161 | ```
162 | export NPC_SERVER_ADDR=1.1.1.1:8024
163 | export NPC_SERVER_VKEY=xxxxx
164 | ```
165 | 直接执行./npc即可运行
166 |
167 | **在配置文件启动模式下:**
168 | ```ini
169 | [common]
170 | server_addr={{.NPC_SERVER_ADDR}}
171 | conn_type=tcp
172 | vkey={{.NPC_SERVER_VKEY}}
173 | auto_reconnection=true
174 | [web]
175 | host={{.NPC_WEB_HOST}}
176 | target_addr={{.NPC_WEB_TARGET}}
177 | ```
178 | 在配置文件中填入相应的环境变量名称,npc将自动进行渲染配置文件替换环境变量
179 |
180 | ## 健康检查
181 |
182 | 当客户端以配置文件模式启动时,支持多节点的健康检查。配置示例如下
183 |
184 | ```ini
185 | [health_check_test1]
186 | health_check_timeout=1
187 | health_check_max_failed=3
188 | health_check_interval=1
189 | health_http_url=/
190 | health_check_type=http
191 | health_check_target=127.0.0.1:8083,127.0.0.1:8082
192 |
193 | [health_check_test2]
194 | health_check_timeout=1
195 | health_check_max_failed=3
196 | health_check_interval=1
197 | health_check_type=tcp
198 | health_check_target=127.0.0.1:8083,127.0.0.1:8082
199 | ```
200 | **health关键词必须在开头存在**
201 |
202 | 第一种是http模式,也就是以get的方式请求目标+url,返回状态码为200表示成功
203 |
204 | 第一种是tcp模式,也就是以tcp的方式与目标建立连接,能成功建立连接表示成功
205 |
206 | 如果失败次数超过`health_check_max_failed`,nps则会移除该npc下的所有该目标,如果失败后目标重新上线,nps将自动将目标重新加入。
207 |
208 | 项 | 含义
209 | ---|---
210 | health_check_timeout | 健康检查超时时间
211 | health_check_max_failed | 健康检查允许失败次数
212 | health_check_interval | 健康检查间隔
213 | health_check_type | 健康检查类型
214 | health_check_target | 健康检查目标,多个以逗号(,)分隔
215 | health_check_type | 健康检查类型
216 | health_http_url | 健康检查url,仅http模式适用
217 |
218 | ## 日志输出
219 |
220 | 日志输出级别
221 |
222 | **对于npc:**
223 | ```
224 | -log_level=0~7 -log_path=npc.log
225 | ```
226 | ```
227 | LevelEmergency->0 LevelAlert->1
228 |
229 | LevelCritical->2 LevelError->3
230 |
231 | LevelWarning->4 LevelNotice->5
232 |
233 | LevelInformational->6 LevelDebug->7
234 | ```
235 | 默认为全输出,级别为0到7
236 |
237 | **对于nps:**
238 |
239 | 在`nps.conf`中设置相关配置即可
240 |
241 | ## pprof性能分析与调试
242 |
243 | 可在服务端与客户端配置中开启pprof端口,用于性能分析与调试,注释或留空相应参数为关闭。
244 |
245 | 默认为关闭状态
246 |
247 | ## 自定义客户端超时检测断开时间
248 |
249 | 客户端与服务端间会间隔5s相互发送延迟测量包,这个时间间隔不可修改。
250 | 可修改延迟测量包丢包的次数,默认为60也就是5分钟都收不到一个延迟测量回包,则会断开客户端连接。
251 | 值得注意的是需要客户端的socket关闭,才会进行重连,也就是当客户端无法收到服务端的fin包时,只有客户端自行关闭socket才行。
252 | 也就是假如服务端设置为较低值,而客户端设置较高值,而此时服务端断开连接而客户端无法收到服务端的fin包,客户端也会继续等着直到触发客户端的超时设置。
253 |
254 | 在`nps.conf`或`npc.conf`中设置`disconnect_timeout`即可,客户端还可附带`-disconnect_timeout=60`参数启动
255 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Document
6 |
7 |
8 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/docs/install.md:
--------------------------------------------------------------------------------
1 | # 安装
2 | ## 安装包安装
3 | [releases](https://github.com/ehang-io/nps/releases)
4 |
5 | 下载对应的系统版本即可,服务端和客户端是单独的
6 |
7 | ## 源码安装
8 | - 安装源码
9 | ```go get -u ehang.io/nps```
10 | - 编译
11 |
12 | 服务端```go build cmd/nps/nps.go```
13 |
14 | 客户端```go build cmd/npc/npc.go```
15 |
16 | ## docker安装
17 | > [server](https://hub.docker.com/r/ffdfgdfg/nps)
18 | > [client](https://hub.docker.com/r/ffdfgdfg/npc)
19 |
--------------------------------------------------------------------------------
/docs/introduction.md:
--------------------------------------------------------------------------------
1 | 
2 | # 介绍
3 |
4 | 可在网页上配置和管理各个tcp、udp隧道、内网站点代理,http、https解析等,功能强大,操作方便。
5 |
--------------------------------------------------------------------------------
/docs/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/docs/logo.png
--------------------------------------------------------------------------------
/docs/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/npc_extend.md:
--------------------------------------------------------------------------------
1 | # 增强功能
2 | ## nat类型检测
3 | ```
4 | ./npc nat -stun_addr=stun.stunprotocol.org:3478
5 | ```
6 | 如果p2p双方都是Symmetric Nat,肯定不能成功,其他组合都有较大成功率。`stun_addr`可以指定stun服务器地址。
7 | ## 状态检查
8 | ```
9 | ./npc status -config=npc配置文件路径
10 | ```
11 | ## 重载配置文件
12 | ```
13 | ./npc restart -config=npc配置文件路径
14 | ```
15 |
16 | ## 通过代理连接nps
17 | 有时候运行npc的内网机器无法直接访问外网,此时可以可以通过socks5代理连接nps
18 |
19 | 对于配置文件方式启动,设置
20 | ```ini
21 | [common]
22 | proxy_url=socks5://111:222@127.0.0.1:8024
23 | ```
24 | 对于无配置文件模式,加上参数
25 |
26 | ```
27 | -proxy=socks5://111:222@127.0.0.1:8024
28 | ```
29 | 支持socks5和http两种模式
30 |
31 | 即socks5://username:password@ip:port
32 |
33 | 或http://username:password@ip:port
34 |
35 | ## 群晖支持
36 | 可在releases中下载spk群晖套件,例如`npc_x64-6.1_0.19.0-1.spk`
37 |
--------------------------------------------------------------------------------
/docs/npc_sdk.md:
--------------------------------------------------------------------------------
1 | # npc sdk文档
2 |
3 | ```
4 | 命令行模式启动客户端
5 | 从v0.26.10开始,此函数会阻塞,直到客户端退出返回,请自行管理是否重连
6 | p0->连接地址
7 | p1->vkey
8 | p2->连接类型(tcp or udp)
9 | p3->连接代理
10 |
11 | extern GoInt StartClientByVerifyKey(char* p0, char* p1, char* p2, char* p3);
12 |
13 | 查看当前启动的客户端状态,在线为1,离线为0
14 | extern GoInt GetClientStatus();
15 |
16 | 关闭客户端
17 | extern void CloseClient();
18 |
19 | 获取当前客户端版本
20 | extern char* Version();
21 |
22 | 获取日志,实时更新
23 | extern char* Logs();
24 | ```
25 |
--------------------------------------------------------------------------------
/docs/nps_extend.md:
--------------------------------------------------------------------------------
1 | # 增强功能
2 | ## 使用https
3 |
4 | **方式一:** 类似于nginx实现https的处理
5 |
6 | 在配置文件中将https_proxy_port设置为443或者其他你想配置的端口,将`https_just_proxy`设置为false,nps 重启后,在web管理界面,域名新增或修改界面中修改域名证书和密钥。
7 |
8 | **此外:** 可以在`nps.conf`中设置一个默认的https配置,当遇到未在web中设置https证书的域名解析时,将自动使用默认证书,另还有一种情况就是对于某些请求的clienthello不携带sni扩展信息,nps也将自动使用默认证书
9 |
10 |
11 | **方式二:** 在内网对应服务器上设置https
12 |
13 | 在`nps.conf`中将`https_just_proxy`设置为true,并且打开`https_proxy_port`端口,然后nps将直接转发https请求到内网服务器上,由内网服务器进行https处理
14 |
15 | ## 与nginx配合
16 |
17 | 有时候我们还需要在云服务器上运行nginx来保证静态文件缓存等,本代理可和nginx配合使用,在配置文件中将httpProxyPort设置为非80端口,并在nginx中配置代理,例如httpProxyPort为8010时
18 | ```
19 | server {
20 | listen 80;
21 | server_name *.proxy.com;
22 | location / {
23 | proxy_set_header Host $http_host;
24 | proxy_pass http://127.0.0.1:8010;
25 | }
26 | }
27 | ```
28 | 如需使用https也可在nginx监听443端口并配置ssl,并将本代理的httpsProxyPort设置为空关闭https即可,例如httpProxyPort为8020时
29 |
30 | ```
31 | server {
32 | listen 443;
33 | server_name *.proxy.com;
34 | ssl on;
35 | ssl_certificate certificate.crt;
36 | ssl_certificate_key private.key;
37 | ssl_session_timeout 5m;
38 | ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
39 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
40 | ssl_prefer_server_ciphers on;
41 | location / {
42 | proxy_set_header Host $http_host;
43 | proxy_pass http://127.0.0.1:8020;
44 | }
45 | }
46 | ```
47 | ## web管理使用https
48 | 如果web管理需要使用https,可以在配置文件`nps.conf`中设置`web_open_ssl=true`,并配置`web_cert_file`和`web_key_file`
49 | ## web使用Caddy代理
50 |
51 | 如果将web配置到Caddy代理,实现子路径访问nps,可以这样配置.
52 |
53 | 假设我们想通过 `http://caddy_ip:caddy_port/nps` 来访问后台, Caddyfile 这样配置:
54 |
55 | ```Caddyfile
56 | caddy_ip:caddy_port/nps {
57 | ##server_ip 为 nps 服务器IP
58 | ##web_port 为 nps 后台端口
59 | proxy / http://server_ip:web_port/nps {
60 | transparent
61 | }
62 | }
63 | ```
64 |
65 | nps.conf 修改 `web_base_url` 为 `/nps` 即可
66 | ```
67 | web_base_url=/nps
68 | ```
69 |
70 |
71 | ## 关闭代理
72 |
73 | 如需关闭http代理可在配置文件中将http_proxy_port设置为空,如需关闭https代理可在配置文件中将https_proxy_port设置为空。
74 |
75 | ## 流量数据持久化
76 | 服务端支持将流量数据持久化,默认情况下是关闭的,如果有需求可以设置`nps.conf`中的`flow_store_interval`参数,单位为分钟
77 |
78 | **注意:** nps不会持久化通过公钥连接的客户端
79 | ## 系统信息显示
80 | nps服务端支持在web上显示和统计服务器的相关信息,但默认一些统计图表是关闭的,如需开启请在`nps.conf`中设置`system_info_display=true`
81 |
82 | ## 自定义客户端连接密钥
83 | web上可以自定义客户端连接的密钥,但是必须具有唯一性
84 | ## 关闭公钥访问
85 | 可以将`nps.conf`中的`public_vkey`设置为空或者删除
86 |
87 | ## 关闭web管理
88 | 可以将`nps.conf`中的`web_port`设置为空或者删除
89 |
90 | ## 服务端多用户登陆
91 | 如果将`nps.conf`中的`allow_user_login`设置为true,服务端web将支持多用户登陆,登陆用户名为user,默认密码为每个客户端的验证密钥,登陆后可以进入客户端编辑修改web登陆的用户名和密码,默认该功能是关闭的。
92 |
93 | ## 用户注册功能
94 | nps服务端支持用户注册功能,可将`nps.conf`中的`allow_user_register`设置为true,开启后登陆页将会有有注册功能,
95 |
96 | ## 监听指定ip
97 |
98 | nps支持每个隧道监听不同的服务端端口,在`nps.conf`中设置`allow_multi_ip=true`后,可在web中控制,或者npc配置文件中(可忽略,默认为0.0.0.0)
99 | ```ini
100 | server_ip=xxx
101 | ```
102 | ## 代理到服务端本地
103 | 在使用nps监听80或者443端口时,默认是将所有的请求都会转发到内网上,但有时候我们的nps服务器的上一些服务也需要使用这两个端口,nps提供类似于`nginx` `proxy_pass` 的功能,支持将代理到服务器本地,该功能支持域名解析,tcp、udp隧道,默认关闭。
104 |
105 | **即:** 假设在nps的vps服务器上有一个服务使用5000端口,这时候nps占用了80端口和443,我们想能使用一个域名通过http(s)访问到5000的服务。
106 |
107 | **使用方式:** 在`nps.conf`中设置`allow_local_proxy=true`,然后在web上设置想转发的隧道或者域名然后选择转发到本地选项即可成功。
108 |
--------------------------------------------------------------------------------
/docs/nps_use.md:
--------------------------------------------------------------------------------
1 | # 使用
2 | **提示:使用web模式时,服务端执行文件必须在项目根目录,否则无法正确加载配置文件**
3 |
4 | ## web管理
5 |
6 | 进入web界面,公网ip:web界面端口(默认8080),密码默认为123
7 |
8 | 进入web管理界面,有详细的说明
9 |
10 | ## 服务端配置文件重载
11 | 对于linux、darwin
12 | ```shell
13 | sudo nps reload
14 | ```
15 | 对于windows
16 | ```shell
17 | nps.exe reload
18 | ```
19 | **说明:** 仅支持部分配置重载,例如`allow_user_login` `auth_crypt_key` `auth_key` `web_username` `web_password` 等,未来将支持更多
20 |
21 |
22 | ## 服务端停止或重启
23 | 对于linux、darwin
24 | ```shell
25 | sudo nps stop|restart
26 | ```
27 | 对于windows
28 | ```shell
29 | nps.exe stop|restart
30 | ```
31 | ## 服务端更新
32 | 请首先执行 `sudo nps stop` 或者 `nps.exe stop` 停止运行,然后
33 |
34 | 对于linux
35 | ```shell
36 | sudo nps-update update
37 | ```
38 | 对于windows
39 | ```shell
40 | nps-update.exe update
41 | ```
42 |
43 | 更新完成后,执行执行 `sudo nps start` 或者 `nps.exe start` 重新运行即可完成升级
44 |
45 | 如果无法更新成功,可以直接自行下载releases压缩包然后覆盖原有的nps二进制文件和web目录
46 |
47 | 注意:`nps install` 之后的 nps 不在原位置,请使用 `whereis nps` 查找具体目录覆盖 nps 二进制文件
48 |
--------------------------------------------------------------------------------
/docs/run.md:
--------------------------------------------------------------------------------
1 | # 启动
2 | ## 服务端
3 | 下载完服务器压缩包后,解压,然后进入解压后的文件夹
4 |
5 | - 执行安装命令
6 |
7 | 对于linux|darwin ```sudo ./nps install```
8 |
9 | 对于windows,管理员身份运行cmd,进入安装目录 ```nps.exe install```
10 |
11 | - 启动
12 |
13 | 对于linux|darwin ```sudo nps start```
14 |
15 | 对于windows,管理员身份运行cmd,进入程序目录 ```nps.exe start```
16 |
17 | ```安装后windows配置文件位于 C:\Program Files\nps,linux和darwin位于/etc/nps```
18 |
19 | 停止和重启可用,stop和restart
20 |
21 | **如果发现没有启动成功,可以使用`nps(.exe) stop`,然后运行`nps.(exe)`运行调试,或查看日志**(Windows日志文件位于当前运行目录下,linux和darwin位于/var/log/nps.log)
22 | - 访问服务端ip:web服务端口(默认为8080)
23 | - 使用用户名和密码登陆(默认admin/123,正式使用一定要更改)
24 | - 创建客户端
25 |
26 | ## 客户端
27 | - 下载客户端安装包并解压,进入到解压目录
28 | - 点击web管理中客户端前的+号,复制启动命令
29 | - 执行启动命令,linux直接执行即可,windows将./npc换成npc.exe用**cmd执行**
30 |
31 | 如果使用`powershell`运行,**请将ip括起来!**
32 |
33 | 如果需要注册到系统服务可查看[注册到系统服务](/use?id=注册到系统服务)
34 |
35 | ## 版本检查
36 | - 对客户端以及服务的均可以使用参数`-version`打印版本
37 | - `nps -version`或`./nps -version`
38 | - `npc -version`或`./npc -version`
39 |
40 | ## 配置
41 | - 客户端连接后,在web中配置对应穿透服务即可
42 | - 可以查看[使用示例](/example)
43 |
--------------------------------------------------------------------------------
/docs/server_config.md:
--------------------------------------------------------------------------------
1 | # 服务端配置文件
2 | - /etc/nps/conf/nps.conf
3 |
4 | 名称 | 含义
5 | ---|---
6 | web_port | web管理端口
7 | web_password | web界面管理密码
8 | web_username | web界面管理账号
9 | web_base_url | web管理主路径,用于将web管理置于代理子路径后面
10 | bridge_port | 服务端客户端通信端口
11 | https_proxy_port | 域名代理https代理监听端口
12 | http_proxy_port | 域名代理http代理监听端口
13 | auth_key|web api密钥
14 | bridge_type|客户端与服务端连接方式kcp或tcp
15 | public_vkey|客户端以配置文件模式启动时的密钥,设置为空表示关闭客户端配置文件连接模式
16 | ip_limit|是否限制ip访问,true或false或忽略
17 | flow_store_interval|服务端流量数据持久化间隔,单位分钟,忽略表示不持久化
18 | log_level|日志输出级别
19 | auth_crypt_key | 获取服务端authKey时的aes加密密钥,16位
20 | p2p_ip| 服务端Ip,使用p2p模式必填
21 | p2p_port|p2p模式开启的udp端口
22 | pprof_ip|debug pprof 服务端ip
23 | pprof_port|debug pprof 端口
24 | disconnect_timeout|客户端连接超时,单位 5s,默认值 60,即 300s = 5mins
25 |
--------------------------------------------------------------------------------
/docs/thanks.md:
--------------------------------------------------------------------------------
1 | Thanks [jetbrains](https://www.jetbrains.com/?from=nps) for providing development tools for nps
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/docs/use.md:
--------------------------------------------------------------------------------
1 | # 基本使用
2 | ## 无配置文件模式
3 | 此模式的各种配置在服务端web管理中完成,客户端除运行一条命令外无需任何其他设置
4 | ```
5 | ./npc -server=ip:port -vkey=web界面中显示的密钥
6 | ```
7 | ## 注册到系统服务(开机启动、守护进程)
8 | 对于linux、darwin
9 | - 注册:`sudo ./npc install 其他参数(例如-server=xx -vkey=xx或者-config=xxx)`
10 | - 启动:`sudo npc start`
11 | - 停止:`sudo npc stop`
12 | - 如果需要更换命令内容需要先卸载`./npc uninstall`,再重新注册
13 |
14 | 对于windows,使用管理员身份运行cmd
15 |
16 | - 注册:`npc.exe install 其他参数(例如-server=xx -vkey=xx或者-config=xxx)`
17 | - 启动:`npc.exe start`
18 | - 停止:`npc.exe stop`
19 | - 如果需要更换命令内容需要先卸载`npc.exe uninstall`,再重新注册
20 | - 如果需要当客户端退出时自动重启客户端,请按照如图所示配置
21 | 
22 |
23 | 注册到服务后,日志文件windows位于当前目录下,linux和darwin位于/var/log/npc.log
24 |
25 | ## 客户端更新
26 | 首先进入到对于的客户端二进制文件目录
27 |
28 | 请首先执行`sudo npc stop`或者`npc.exe stop`停止运行,然后
29 |
30 | 对于linux
31 | ```shell
32 | sudo npc-update update
33 | ```
34 | 对于windows
35 | ```shell
36 | npc-update.exe update
37 | ```
38 |
39 | 更新完成后,执行执行`sudo npc start`或者`npc.exe start`重新运行即可完成升级
40 |
41 | 如果无法更新成功,可以直接自行下载releases压缩包然后覆盖原有的npc二进制文件
42 |
43 | ## 配置文件模式
44 | 此模式使用nps的公钥或者客户端私钥验证,各种配置在客户端完成,同时服务端web也可以进行管理
45 | ```
46 | ./npc -config=npc配置文件路径
47 | ```
48 | ## 配置文件说明
49 | [示例配置文件](https://github.com/ehang-io/nps/tree/master/conf/npc.conf)
50 | #### 全局配置
51 | ```ini
52 | [common]
53 | server_addr=1.1.1.1:8024
54 | conn_type=tcp
55 | vkey=123
56 | username=111
57 | password=222
58 | compress=true
59 | crypt=true
60 | rate_limit=10000
61 | flow_limit=100
62 | remark=test
63 | max_conn=10
64 | #pprof_addr=0.0.0.0:9999
65 | ```
66 | 项 | 含义
67 | ---|---
68 | server_addr | 服务端ip/域名:port
69 | conn_type | 与服务端通信模式(tcp或kcp)
70 | vkey|服务端配置文件中的密钥(非web)
71 | username|socks5或http(s)密码保护用户名(可忽略)
72 | password|socks5或http(s)密码保护密码(可忽略)
73 | compress|是否压缩传输(true或false或忽略)
74 | crypt|是否加密传输(true或false或忽略)
75 | rate_limit|速度限制,可忽略
76 | flow_limit|流量限制,可忽略
77 | remark|客户端备注,可忽略
78 | max_conn|最大连接数,可忽略
79 | pprof_addr|debug pprof ip:port
80 | #### 域名代理
81 |
82 | ```ini
83 | [common]
84 | server_addr=1.1.1.1:8024
85 | vkey=123
86 | [web1]
87 | host=a.proxy.com
88 | target_addr=127.0.0.1:8080,127.0.0.1:8082
89 | host_change=www.proxy.com
90 | header_set_proxy=nps
91 | ```
92 | 项 | 含义
93 | ---|---
94 | web1 | 备注
95 | host | 域名(http|https都可解析)
96 | target_addr|内网目标,负载均衡时多个目标,逗号隔开
97 | host_change|请求host修改
98 | header_xxx|请求header修改或添加,header_proxy表示添加header proxy:nps
99 |
100 | #### tcp隧道模式
101 |
102 | ```ini
103 | [common]
104 | server_addr=1.1.1.1:8024
105 | vkey=123
106 | [tcp]
107 | mode=tcp
108 | target_addr=127.0.0.1:8080
109 | server_port=9001
110 | ```
111 | 项 | 含义
112 | ---|---
113 | mode | tcp
114 | server_port | 在服务端的代理端口
115 | tartget_addr|内网目标
116 |
117 | #### udp隧道模式
118 |
119 | ```ini
120 | [common]
121 | server_addr=1.1.1.1:8024
122 | vkey=123
123 | [udp]
124 | mode=udp
125 | target_addr=127.0.0.1:8080
126 | server_port=9002
127 | ```
128 | 项 | 含义
129 | ---|---
130 | mode | udp
131 | server_port | 在服务端的代理端口
132 | target_addr|内网目标
133 | #### http代理模式
134 |
135 | ```ini
136 | [common]
137 | server_addr=1.1.1.1:8024
138 | vkey=123
139 | [http]
140 | mode=httpProxy
141 | server_port=9003
142 | ```
143 | 项 | 含义
144 | ---|---
145 | mode | httpProxy
146 | server_port | 在服务端的代理端口
147 | #### socks5代理模式
148 |
149 | ```ini
150 | [common]
151 | server_addr=1.1.1.1:8024
152 | vkey=123
153 | [socks5]
154 | mode=socks5
155 | server_port=9004
156 | multi_account=multi_account.conf
157 | ```
158 | 项 | 含义
159 | ---|---
160 | mode | socks5
161 | server_port | 在服务端的代理端口
162 | multi_account | socks5多账号配置文件(可选),配置后使用basic_username和basic_password无法通过认证
163 | #### 私密代理模式
164 |
165 | ```ini
166 | [common]
167 | server_addr=1.1.1.1:8024
168 | vkey=123
169 | [secret_ssh]
170 | mode=secret
171 | password=ssh2
172 | target_addr=10.1.50.2:22
173 | ```
174 | 项 | 含义
175 | ---|---
176 | mode | secret
177 | password | 唯一密钥
178 | target_addr|内网目标
179 |
180 | #### p2p代理模式
181 |
182 | ```ini
183 | [common]
184 | server_addr=1.1.1.1:8024
185 | vkey=123
186 | [p2p_ssh]
187 | mode=p2p
188 | password=ssh2
189 | target_addr=10.1.50.2:22
190 | ```
191 | 项 | 含义
192 | ---|---
193 | mode | p2p
194 | password | 唯一密钥
195 | target_addr|内网目标
196 |
197 |
198 | #### 文件访问模式
199 | 利用nps提供一个公网可访问的本地文件服务,此模式仅客户端使用配置文件模式方可启动
200 |
201 | ```ini
202 | [common]
203 | server_addr=1.1.1.1:8024
204 | vkey=123
205 | [file]
206 | mode=file
207 | server_port=9100
208 | local_path=/tmp/
209 | strip_pre=/web/
210 | ````
211 |
212 | 项 | 含义
213 | ---|---
214 | mode | file
215 | server_port | 服务端开启的端口
216 | local_path|本地文件目录
217 | strip_pre|前缀
218 |
219 | 对于`strip_pre`,访问公网`ip:9100/web/`相当于访问`/tmp/`目录
220 |
221 | #### 断线重连
222 | ```ini
223 | [common]
224 | auto_reconnection=true
225 | ```
226 |
--------------------------------------------------------------------------------
/docs/webapi.md:
--------------------------------------------------------------------------------
1 | 获取客户端列表
2 |
3 | ```
4 | POST /client/list/
5 | ```
6 |
7 |
8 | | 参数 | 含义 |
9 | | --- | --- |
10 | | search | 搜索 |
11 | | order | 排序asc 正序 desc倒序 |
12 | | offset | 分页(第几页) |
13 | | limit | 条数(分页显示的条数) |
14 |
15 | ***
16 | 获取单个客户端
17 |
18 | ```
19 | POST /client/getclient/
20 | ```
21 |
22 |
23 | | 参数 | 含义 |
24 | | --- | --- |
25 | | id | 客户端id |
26 |
27 | ***
28 | 添加客户端
29 |
30 | ```
31 | POST /client/add/
32 | ```
33 |
34 | | 参数 | 含义 |
35 | | --- | --- |
36 | | remark | 备注 |
37 | | u | basic权限认证用户名 |
38 | | p | basic权限认证密码 |
39 | | limit | 条数(分页显示的条数) |
40 | | vkey | 客户端验证密钥 |
41 | | config\_conn\_allow | 是否允许客户端以配置文件模式连接 1允许 0不允许 |
42 | | compress | 压缩1允许 0不允许 |
43 | | crypt | 是否加密(1或者0)1允许 0不允许 |
44 | | rate\_limit | 带宽限制 单位KB/S 空则为不限制 |
45 | | flow\_limit | 流量限制 单位M 空则为不限制 |
46 | | max\_conn | 客户端最大连接数量 空则为不限制 |
47 | | max\_tunnel | 客户端最大隧道数量 空则为不限制 |
48 |
49 | ***
50 | 修改客户端
51 |
52 | ```
53 | POST /client/edit/
54 | ```
55 |
56 | | 参数 | 含义 |
57 | | --- | --- |
58 | | remark | 备注 |
59 | | u | basic权限认证用户名 |
60 | | p | basic权限认证密码 |
61 | | limit | 条数(分页显示的条数) |
62 | | vkey | 客户端验证密钥 |
63 | | config\_conn\_allow | 是否允许客户端以配置文件模式连接 1允许 0不允许 |
64 | | compress | 压缩1允许 0不允许 |
65 | | crypt | 是否加密(1或者0)1允许 0不允许 |
66 | | rate\_limit | 带宽限制 单位KB/S 空则为不限制 |
67 | | flow\_limit | 流量限制 单位M 空则为不限制 |
68 | | max\_conn | 客户端最大连接数量 空则为不限制 |
69 | | max\_tunnel | 客户端最大隧道数量 空则为不限制 |
70 | | id | 要修改的客户端id |
71 |
72 | ***
73 | 删除客户端
74 |
75 | ```
76 | POST /client/del/
77 | ```
78 |
79 | | 参数 | 含义 |
80 | | --- | --- |
81 | | id | 要删除的客户端id |
82 |
83 | ***
84 | 获取域名解析列表
85 |
86 | ```
87 | POST /index/hostlist/
88 | ```
89 |
90 | | 参数 | 含义 |
91 | | --- | --- |
92 | | search | 搜索(可以搜域名/备注什么的) |
93 | | offset | 分页(第几页) |
94 | | limit | 条数(分页显示的条数) |
95 |
96 | ***
97 | 添加域名解析
98 |
99 | ```
100 | POST /index/addhost/
101 | ```
102 |
103 |
104 | | 参数 | 含义 |
105 | | --- | --- |
106 | | remark | 备注 |
107 | | host | 域名 |
108 | | scheme | 协议类型(三种 all http https) |
109 | | location | url路由 空则为不限制 |
110 | | client\_id | 客户端id |
111 | | target | 内网目标(ip:端口) |
112 | | header | request header 请求头 |
113 | | hostchange | request host 请求主机 |
114 |
115 | ***
116 | 修改域名解析
117 |
118 | ```
119 | POST /index/edithost/
120 | ```
121 |
122 | | 参数 | 含义 |
123 | | --- | --- |
124 | | remark | 备注 |
125 | | host | 域名 |
126 | | scheme | 协议类型(三种 all http https) |
127 | | location | url路由 空则为不限制 |
128 | | client\_id | 客户端id |
129 | | target | 内网目标(ip:端口) |
130 | | header | request header 请求头 |
131 | | hostchange | request host 请求主机 |
132 | | id | 需要修改的域名解析id |
133 |
134 | ***
135 | 删除域名解析
136 |
137 | ```
138 | POST /index/delhost/
139 | ```
140 |
141 | | 参数 | 含义 |
142 | | --- | --- |
143 | | id | 需要删除的域名解析id |
144 |
145 | ***
146 | 获取单条隧道信息
147 |
148 | ```
149 | POST /index/getonetunnel/
150 | ```
151 |
152 | | 参数 | 含义 |
153 | | --- | --- |
154 | | id | 隧道的id |
155 |
156 | ***
157 | 获取隧道列表
158 |
159 | ```
160 | POST /index/gettunnel/
161 | ```
162 |
163 | | 参数 | 含义 |
164 | | --- | --- |
165 | | client\_id | 穿透隧道的客户端id |
166 | | type | 类型tcp udp httpProx socks5 secret p2p |
167 | | search | 搜索 |
168 | | offset | 分页(第几页) |
169 | | limit | 条数(分页显示的条数) |
170 |
171 | ***
172 | 添加隧道
173 |
174 | ```
175 | POST /index/add/
176 | ```
177 |
178 | | 参数 | 含义 |
179 | | --- | --- |
180 | | type | 类型tcp udp httpProx socks5 secret p2p |
181 | | remark | 备注 |
182 | | port | 服务端端口 |
183 | | target | 目标(ip:端口) |
184 | | client\_id | 客户端id |
185 |
186 | ***
187 | 修改隧道
188 |
189 | ```
190 | POST /index/edit/
191 | ```
192 |
193 | | 参数 | 含义 |
194 | | --- | --- |
195 | | type | 类型tcp udp httpProx socks5 secret p2p |
196 | | remark | 备注 |
197 | | port | 服务端端口 |
198 | | target | 目标(ip:端口) |
199 | | client\_id | 客户端id |
200 | | id | 隧道id |
201 |
202 | ***
203 | 删除隧道
204 |
205 | ```
206 | POST /index/del/
207 | ```
208 |
209 | | 参数 | 含义 |
210 | | --- | --- |
211 | | id | 隧道id |
212 |
213 | ***
214 | 隧道停止工作
215 |
216 | ```
217 | POST /index/stop/
218 | ```
219 |
220 | | 参数 | 含义 |
221 | | --- | --- |
222 | | id | 隧道id |
223 |
224 | ***
225 | 隧道开始工作
226 |
227 | ```
228 | POST /index/start/
229 | ```
230 |
231 | | 参数 | 含义 |
232 | | --- | --- |
233 | | id | 隧道id |
234 |
--------------------------------------------------------------------------------
/docs/windows_client_service_configuration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/docs/windows_client_service_configuration.png
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module ehang.io/nps
2 |
3 | go 1.15
4 |
5 | require (
6 | ehang.io/nps-mux v0.0.0-20210407130203-4afa0c10c992
7 | fyne.io/fyne/v2 v2.0.2
8 | github.com/astaxie/beego v1.12.0
9 | github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
10 | github.com/c4milo/unpackit v0.0.0-20170704181138-4ed373e9ef1c
11 | github.com/ccding/go-stun v0.0.0-20180726100737-be486d185f3d
12 | github.com/dsnet/compress v0.0.1 // indirect
13 | github.com/golang/snappy v0.0.3
14 | github.com/hooklift/assert v0.0.0-20170704181755-9d1defd6d214 // indirect
15 | github.com/kardianos/service v1.2.0
16 | github.com/klauspost/cpuid v1.3.1 // indirect
17 | github.com/klauspost/cpuid/v2 v2.0.6 // indirect
18 | github.com/klauspost/pgzip v1.2.1 // indirect
19 | github.com/klauspost/reedsolomon v1.9.12 // indirect
20 | github.com/panjf2000/ants/v2 v2.4.2
21 | github.com/pkg/errors v0.9.1
22 | github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 // indirect
23 | github.com/shirou/gopsutil/v3 v3.21.3
24 | github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect
25 | github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect
26 | github.com/tjfoc/gmsm v1.4.0 // indirect
27 | github.com/xtaci/kcp-go v5.4.20+incompatible
28 | github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae // indirect
29 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
30 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
31 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 // indirect
32 | )
33 |
34 | replace github.com/astaxie/beego => github.com/exfly/beego v1.12.0-export-init
35 |
--------------------------------------------------------------------------------
/gui/npc/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/gui/npc/npc.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "ehang.io/nps/client"
5 | "ehang.io/nps/lib/common"
6 | "ehang.io/nps/lib/daemon"
7 | "ehang.io/nps/lib/version"
8 | "fmt"
9 | "fyne.io/fyne/v2"
10 | "fyne.io/fyne/v2/app"
11 | "fyne.io/fyne/v2/container"
12 | "fyne.io/fyne/v2/layout"
13 | "fyne.io/fyne/v2/widget"
14 | "github.com/astaxie/beego/logs"
15 | "io/ioutil"
16 | "os"
17 | "path"
18 | "runtime"
19 | "strings"
20 | "time"
21 | )
22 |
23 | func main() {
24 | daemon.InitDaemon("npc", common.GetRunPath(), common.GetTmpPath())
25 | logs.SetLogger("store")
26 | application := app.New()
27 | window := application.NewWindow("Npc " + version.VERSION)
28 | window.SetContent(WidgetScreen())
29 | window.Resize(fyne.NewSize(910, 350))
30 |
31 | window.ShowAndRun()
32 |
33 | }
34 |
35 | var (
36 | start bool
37 | closing bool
38 | status = "Start!"
39 | connType = "tcp"
40 | cl = new(client.TRPClient)
41 | refreshCh = make(chan struct{})
42 | )
43 |
44 | func WidgetScreen() fyne.CanvasObject {
45 | return fyne.NewContainerWithLayout(layout.NewBorderLayout(nil, nil, nil, nil),
46 | makeMainTab(),
47 | )
48 | }
49 |
50 | func makeMainTab() *fyne.Container {
51 | serverPort := widget.NewEntry()
52 | serverPort.SetPlaceHolder("Server:Port")
53 |
54 | vKey := widget.NewEntry()
55 | vKey.SetPlaceHolder("Vkey")
56 | radio := widget.NewRadioGroup([]string{"tcp", "kcp"}, func(s string) { connType = s })
57 | radio.Horizontal = true
58 |
59 | button := widget.NewButton(status, func() {
60 | onclick(serverPort.Text, vKey.Text, connType)
61 | })
62 | go func() {
63 | for {
64 | <-refreshCh
65 | button.SetText(status)
66 | }
67 | }()
68 |
69 | lo := widget.NewMultiLineEntry()
70 | lo.Disable()
71 | lo.Resize(fyne.NewSize(910, 250))
72 | slo := container.NewScroll(lo)
73 | slo.Resize(fyne.NewSize(910, 250))
74 | go func() {
75 | for {
76 | time.Sleep(time.Second)
77 | lo.SetText(common.GetLogMsg())
78 | slo.Resize(fyne.NewSize(910, 250))
79 | }
80 | }()
81 |
82 | sp, vk, ct := loadConfig()
83 | if sp != "" && vk != "" && ct != "" {
84 | serverPort.SetText(sp)
85 | vKey.SetText(vk)
86 | connType = ct
87 | radio.SetSelected(ct)
88 | onclick(sp, vk, ct)
89 | }
90 |
91 | return container.NewVBox(
92 | widget.NewLabel("Npc "+version.VERSION),
93 | serverPort,
94 | vKey,
95 | radio,
96 | button,
97 | slo,
98 | )
99 | }
100 |
101 | func onclick(s, v, c string) {
102 | start = !start
103 | if start {
104 | closing = false
105 | status = "Stop!"
106 | // init the npc
107 | fmt.Println("submit", s, v, c)
108 | sp, vk, ct := loadConfig()
109 | if sp != s || vk != v || ct != c {
110 | saveConfig(s, v, c)
111 | }
112 | go func() {
113 | for {
114 | cl = client.NewRPClient(s, v, c, "", nil, 60)
115 | status = "Stop!"
116 | refreshCh <- struct{}{}
117 | cl.Start()
118 | logs.Warn("client closed, reconnecting in 5 seconds...")
119 | if closing {
120 | return
121 | }
122 | status = "Reconnecting..."
123 | refreshCh <- struct{}{}
124 | time.Sleep(time.Second * 5)
125 | }
126 | }()
127 | } else {
128 | // close the npc
129 | status = "Start!"
130 | closing = true
131 | if cl != nil {
132 | go cl.Close()
133 | cl = nil
134 | }
135 | }
136 | refreshCh <- struct{}{}
137 | }
138 |
139 | func getDir() (dir string, err error) {
140 | if runtime.GOOS != "android" {
141 | dir, err = os.UserConfigDir()
142 | if err != nil {
143 | return
144 | }
145 | } else {
146 | dir = "/data/data/org.nps.client/files"
147 | }
148 | return
149 | }
150 |
151 | func saveConfig(host, vkey, connType string) {
152 | data := strings.Join([]string{host, vkey, connType}, "\n")
153 | ph, err := getDir()
154 | if err != nil {
155 | logs.Warn("not found config dir")
156 | return
157 | }
158 | _ = os.Remove(path.Join(ph, "npc.conf"))
159 | f, err := os.OpenFile(path.Join(ph, "npc.conf"), os.O_CREATE|os.O_WRONLY, 0644)
160 | defer f.Close()
161 | if err != nil {
162 | logs.Error(err)
163 | return
164 | }
165 | if _, err := f.Write([]byte(data)); err != nil {
166 | _ = f.Close() // ignore error; Write error takes precedence
167 | logs.Error(err)
168 | return
169 | }
170 | }
171 |
172 | func loadConfig() (host, vkey, connType string) {
173 | ph, err := getDir()
174 | if err != nil {
175 | logs.Warn("not found config dir")
176 | return
177 | }
178 | f, err := os.OpenFile(path.Join(ph, "npc.conf"), os.O_RDONLY, 0644)
179 | defer f.Close()
180 | if err != nil {
181 | logs.Error(err)
182 | return
183 | }
184 | data, err := ioutil.ReadAll(f)
185 | if err != nil {
186 | logs.Error(err)
187 | return
188 | }
189 | li := strings.Split(string(data), "\n")
190 | host = li[0]
191 | vkey = li[1]
192 | connType = li[2]
193 | return
194 | }
195 |
--------------------------------------------------------------------------------
/image/cpu1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/image/cpu1.png
--------------------------------------------------------------------------------
/image/cpu2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/image/cpu2.png
--------------------------------------------------------------------------------
/image/donation_wx.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/image/donation_wx.png
--------------------------------------------------------------------------------
/image/donation_zfb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/image/donation_zfb.png
--------------------------------------------------------------------------------
/image/http.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/image/http.png
--------------------------------------------------------------------------------
/image/httpProxy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/image/httpProxy.png
--------------------------------------------------------------------------------
/image/qps.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/image/qps.png
--------------------------------------------------------------------------------
/image/sock5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/image/sock5.png
--------------------------------------------------------------------------------
/image/speed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/image/speed.png
--------------------------------------------------------------------------------
/image/tcp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/image/tcp.png
--------------------------------------------------------------------------------
/image/udp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/image/udp.png
--------------------------------------------------------------------------------
/image/web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/image/web.png
--------------------------------------------------------------------------------
/image/web2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/image/web2.png
--------------------------------------------------------------------------------
/lib/cache/lru.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "container/list"
5 | "sync"
6 | )
7 |
8 | // Cache is an LRU cache. It is safe for concurrent access.
9 | type Cache struct {
10 | // MaxEntries is the maximum number of cache entries before
11 | // an item is evicted. Zero means no limit.
12 | MaxEntries int
13 |
14 | //Execute this callback function when an element is culled
15 | OnEvicted func(key Key, value interface{})
16 |
17 | ll *list.List //list
18 | cache sync.Map
19 | }
20 |
21 | // A Key may be any value that is comparable. See http://golang.org/ref/spec#Comparison_operators
22 | type Key interface{}
23 |
24 | type entry struct {
25 | key Key
26 | value interface{}
27 | }
28 |
29 | // New creates a new Cache.
30 | // If maxEntries is 0, the cache has no length limit.
31 | // that eviction is done by the caller.
32 | func New(maxEntries int) *Cache {
33 | return &Cache{
34 | MaxEntries: maxEntries,
35 | ll: list.New(),
36 | //cache: make(map[interface{}]*list.Element),
37 | }
38 | }
39 |
40 | // If the key value already exists, move the key to the front
41 | func (c *Cache) Add(key Key, value interface{}) {
42 | if ee, ok := c.cache.Load(key); ok {
43 | c.ll.MoveToFront(ee.(*list.Element)) // move to the front
44 | ee.(*list.Element).Value.(*entry).value = value
45 | return
46 | }
47 | ele := c.ll.PushFront(&entry{key, value})
48 | c.cache.Store(key, ele)
49 | if c.MaxEntries != 0 && c.ll.Len() > c.MaxEntries { // Remove the oldest element if the limit is exceeded
50 | c.RemoveOldest()
51 | }
52 | }
53 |
54 | // Get looks up a key's value from the cache.
55 | func (c *Cache) Get(key Key) (value interface{}, ok bool) {
56 | if ele, hit := c.cache.Load(key); hit {
57 | c.ll.MoveToFront(ele.(*list.Element))
58 | return ele.(*list.Element).Value.(*entry).value, true
59 | }
60 | return
61 | }
62 |
63 | // Remove removes the provided key from the cache.
64 | func (c *Cache) Remove(key Key) {
65 | if ele, hit := c.cache.Load(key); hit {
66 | c.removeElement(ele.(*list.Element))
67 | }
68 | }
69 |
70 | // RemoveOldest removes the oldest item from the cache.
71 | func (c *Cache) RemoveOldest() {
72 | ele := c.ll.Back()
73 | if ele != nil {
74 | c.removeElement(ele)
75 | }
76 | }
77 |
78 | func (c *Cache) removeElement(e *list.Element) {
79 | c.ll.Remove(e)
80 | kv := e.Value.(*entry)
81 | c.cache.Delete(kv.key)
82 | if c.OnEvicted != nil {
83 | c.OnEvicted(kv.key, kv.value)
84 | }
85 | }
86 |
87 | // Len returns the number of items in the cache.
88 | func (c *Cache) Len() int {
89 | return c.ll.Len()
90 | }
91 |
92 | // Clear purges all stored items from the cache.
93 | func (c *Cache) Clear() {
94 | if c.OnEvicted != nil {
95 | c.cache.Range(func(key, value interface{}) bool {
96 | kv := value.(*list.Element).Value.(*entry)
97 | c.OnEvicted(kv.key, kv.value)
98 | return true
99 | })
100 | }
101 | c.ll = nil
102 | }
103 |
--------------------------------------------------------------------------------
/lib/common/const.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | const (
4 | CONN_DATA_SEQ = "*#*" //Separator
5 | VERIFY_EER = "vkey"
6 | VERIFY_SUCCESS = "sucs"
7 | WORK_MAIN = "main"
8 | WORK_CHAN = "chan"
9 | WORK_CONFIG = "conf"
10 | WORK_REGISTER = "rgst"
11 | WORK_SECRET = "sert"
12 | WORK_FILE = "file"
13 | WORK_P2P = "p2pm"
14 | WORK_P2P_VISITOR = "p2pv"
15 | WORK_P2P_PROVIDER = "p2pp"
16 | WORK_P2P_CONNECT = "p2pc"
17 | WORK_P2P_SUCCESS = "p2ps"
18 | WORK_P2P_END = "p2pe"
19 | WORK_P2P_LAST = "p2pl"
20 | WORK_STATUS = "stus"
21 | RES_MSG = "msg0"
22 | RES_CLOSE = "clse"
23 | NEW_UDP_CONN = "udpc" //p2p udp conn
24 | NEW_TASK = "task"
25 | NEW_CONF = "conf"
26 | NEW_HOST = "host"
27 | CONN_TCP = "tcp"
28 | CONN_UDP = "udp"
29 | CONN_TEST = "TST"
30 | UnauthorizedBytes = `HTTP/1.1 401 Unauthorized
31 | Content-Type: text/plain; charset=utf-8
32 | WWW-Authenticate: Basic realm="easyProxy"
33 |
34 | 401 Unauthorized`
35 | ConnectionFailBytes = `HTTP/1.1 404 Not Found
36 |
37 | `
38 | )
39 |
--------------------------------------------------------------------------------
/lib/common/logs.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "github.com/astaxie/beego/logs"
5 | "time"
6 | )
7 |
8 | const MaxMsgLen = 5000
9 |
10 | var logMsgs string
11 |
12 | func init() {
13 | logs.Register("store", func() logs.Logger {
14 | return new(StoreMsg)
15 | })
16 | }
17 |
18 | func GetLogMsg() string {
19 | return logMsgs
20 | }
21 |
22 | type StoreMsg struct {
23 | }
24 |
25 | func (lg *StoreMsg) Init(config string) error {
26 | return nil
27 | }
28 |
29 | func (lg *StoreMsg) WriteMsg(when time.Time, msg string, level int) error {
30 | m := when.Format("2006-01-02 15:04:05") + " " + msg + "\r\n"
31 | if len(logMsgs) > MaxMsgLen {
32 | start := MaxMsgLen - len(m)
33 | if start <= 0 {
34 | start = MaxMsgLen
35 | }
36 | logMsgs = logMsgs[start:]
37 | }
38 | logMsgs += m
39 | return nil
40 | }
41 |
42 | func (lg *StoreMsg) Destroy() {
43 | return
44 | }
45 |
46 | func (lg *StoreMsg) Flush() {
47 | return
48 | }
49 |
--------------------------------------------------------------------------------
/lib/common/netpackager.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "errors"
7 | "io"
8 | "io/ioutil"
9 | "net"
10 | "strconv"
11 | )
12 |
13 | type NetPackager interface {
14 | Pack(writer io.Writer) (err error)
15 | UnPack(reader io.Reader) (err error)
16 | }
17 |
18 | const (
19 | ipV4 = 1
20 | domainName = 3
21 | ipV6 = 4
22 | )
23 |
24 | type UDPHeader struct {
25 | Rsv uint16
26 | Frag uint8
27 | Addr *Addr
28 | }
29 |
30 | func NewUDPHeader(rsv uint16, frag uint8, addr *Addr) *UDPHeader {
31 | return &UDPHeader{
32 | Rsv: rsv,
33 | Frag: frag,
34 | Addr: addr,
35 | }
36 | }
37 |
38 | type Addr struct {
39 | Type uint8
40 | Host string
41 | Port uint16
42 | }
43 |
44 | func (addr *Addr) String() string {
45 | return net.JoinHostPort(addr.Host, strconv.Itoa(int(addr.Port)))
46 | }
47 |
48 | func (addr *Addr) Decode(b []byte) error {
49 | addr.Type = b[0]
50 | pos := 1
51 | switch addr.Type {
52 | case ipV4:
53 | addr.Host = net.IP(b[pos : pos+net.IPv4len]).String()
54 | pos += net.IPv4len
55 | case ipV6:
56 | addr.Host = net.IP(b[pos : pos+net.IPv6len]).String()
57 | pos += net.IPv6len
58 | case domainName:
59 | addrlen := int(b[pos])
60 | pos++
61 | addr.Host = string(b[pos : pos+addrlen])
62 | pos += addrlen
63 | default:
64 | return errors.New("decode error")
65 | }
66 |
67 | addr.Port = binary.BigEndian.Uint16(b[pos:])
68 |
69 | return nil
70 | }
71 |
72 | func (addr *Addr) Encode(b []byte) (int, error) {
73 | b[0] = addr.Type
74 | pos := 1
75 | switch addr.Type {
76 | case ipV4:
77 | ip4 := net.ParseIP(addr.Host).To4()
78 | if ip4 == nil {
79 | ip4 = net.IPv4zero.To4()
80 | }
81 | pos += copy(b[pos:], ip4)
82 | case domainName:
83 | b[pos] = byte(len(addr.Host))
84 | pos++
85 | pos += copy(b[pos:], []byte(addr.Host))
86 | case ipV6:
87 | ip16 := net.ParseIP(addr.Host).To16()
88 | if ip16 == nil {
89 | ip16 = net.IPv6zero.To16()
90 | }
91 | pos += copy(b[pos:], ip16)
92 | default:
93 | b[0] = ipV4
94 | copy(b[pos:pos+4], net.IPv4zero.To4())
95 | pos += 4
96 | }
97 | binary.BigEndian.PutUint16(b[pos:], addr.Port)
98 | pos += 2
99 |
100 | return pos, nil
101 | }
102 |
103 | func (h *UDPHeader) Write(w io.Writer) error {
104 | b := BufPoolUdp.Get().([]byte)
105 | defer BufPoolUdp.Put(b)
106 |
107 | binary.BigEndian.PutUint16(b[:2], h.Rsv)
108 | b[2] = h.Frag
109 |
110 | addr := h.Addr
111 | if addr == nil {
112 | addr = &Addr{}
113 | }
114 | length, _ := addr.Encode(b[3:])
115 |
116 | _, err := w.Write(b[:3+length])
117 | return err
118 | }
119 |
120 | type UDPDatagram struct {
121 | Header *UDPHeader
122 | Data []byte
123 | }
124 |
125 | func ReadUDPDatagram(r io.Reader) (*UDPDatagram, error) {
126 | b := BufPoolUdp.Get().([]byte)
127 | defer BufPoolUdp.Put(b)
128 |
129 | // when r is a streaming (such as TCP connection), we may read more than the required data,
130 | // but we don't know how to handle it. So we use io.ReadFull to instead of io.ReadAtLeast
131 | // to make sure that no redundant data will be discarded.
132 | n, err := io.ReadFull(r, b[:5])
133 | if err != nil {
134 | return nil, err
135 | }
136 |
137 | header := &UDPHeader{
138 | Rsv: binary.BigEndian.Uint16(b[:2]),
139 | Frag: b[2],
140 | }
141 |
142 | atype := b[3]
143 | hlen := 0
144 | switch atype {
145 | case ipV4:
146 | hlen = 10
147 | case ipV6:
148 | hlen = 22
149 | case domainName:
150 | hlen = 7 + int(b[4])
151 | default:
152 | return nil, errors.New("addr not support")
153 | }
154 | dlen := int(header.Rsv)
155 | if dlen == 0 { // standard SOCKS5 UDP datagram
156 | extra, err := ioutil.ReadAll(r) // we assume no redundant data
157 | if err != nil {
158 | return nil, err
159 | }
160 | copy(b[n:], extra)
161 | n += len(extra) // total length
162 | dlen = n - hlen // data length
163 | } else { // extended feature, for UDP over TCP, using reserved field as data length
164 | if _, err := io.ReadFull(r, b[n:hlen+dlen]); err != nil {
165 | return nil, err
166 | }
167 | n = hlen + dlen
168 | }
169 | header.Addr = new(Addr)
170 | if err := header.Addr.Decode(b[3:hlen]); err != nil {
171 | return nil, err
172 | }
173 | data := make([]byte, dlen)
174 | copy(data, b[hlen:n])
175 | d := &UDPDatagram{
176 | Header: header,
177 | Data: data,
178 | }
179 | return d, nil
180 | }
181 |
182 | func NewUDPDatagram(header *UDPHeader, data []byte) *UDPDatagram {
183 | return &UDPDatagram{
184 | Header: header,
185 | Data: data,
186 | }
187 | }
188 |
189 | func (d *UDPDatagram) Write(w io.Writer) error {
190 | h := d.Header
191 | if h == nil {
192 | h = &UDPHeader{}
193 | }
194 | buf := bytes.Buffer{}
195 | if err := h.Write(&buf); err != nil {
196 | return err
197 | }
198 | if _, err := buf.Write(d.Data); err != nil {
199 | return err
200 | }
201 |
202 | _, err := buf.WriteTo(w)
203 | return err
204 | }
205 |
206 | func ToSocksAddr(addr net.Addr) *Addr {
207 | host := "0.0.0.0"
208 | port := 0
209 | if addr != nil {
210 | h, p, _ := net.SplitHostPort(addr.String())
211 | host = h
212 | port, _ = strconv.Atoi(p)
213 | }
214 | return &Addr{
215 | Type: ipV4,
216 | Host: host,
217 | Port: uint16(port),
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/lib/common/pool.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | const PoolSize = 64 * 1024
8 | const PoolSizeSmall = 100
9 | const PoolSizeUdp = 1472 + 200
10 | const PoolSizeCopy = 32 << 10
11 |
12 | var BufPool = sync.Pool{
13 | New: func() interface{} {
14 | return make([]byte, PoolSize)
15 | },
16 | }
17 |
18 | var BufPoolUdp = sync.Pool{
19 | New: func() interface{} {
20 | return make([]byte, PoolSizeUdp)
21 | },
22 | }
23 | var BufPoolMax = sync.Pool{
24 | New: func() interface{} {
25 | return make([]byte, PoolSize)
26 | },
27 | }
28 | var BufPoolSmall = sync.Pool{
29 | New: func() interface{} {
30 | return make([]byte, PoolSizeSmall)
31 | },
32 | }
33 | var BufPoolCopy = sync.Pool{
34 | New: func() interface{} {
35 | return make([]byte, PoolSizeCopy)
36 | },
37 | }
38 |
39 | func PutBufPoolUdp(buf []byte) {
40 | if cap(buf) == PoolSizeUdp {
41 | BufPoolUdp.Put(buf[:PoolSizeUdp])
42 | }
43 | }
44 |
45 | func PutBufPoolCopy(buf []byte) {
46 | if cap(buf) == PoolSizeCopy {
47 | BufPoolCopy.Put(buf[:PoolSizeCopy])
48 | }
49 | }
50 |
51 | func GetBufPoolCopy() []byte {
52 | return (BufPoolCopy.Get().([]byte))[:PoolSizeCopy]
53 | }
54 |
55 | func PutBufPoolMax(buf []byte) {
56 | if cap(buf) == PoolSize {
57 | BufPoolMax.Put(buf[:PoolSize])
58 | }
59 | }
60 |
61 | type copyBufferPool struct {
62 | pool sync.Pool
63 | }
64 |
65 | func (Self *copyBufferPool) New() {
66 | Self.pool = sync.Pool{
67 | New: func() interface{} {
68 | return make([]byte, PoolSizeCopy, PoolSizeCopy)
69 | },
70 | }
71 | }
72 |
73 | func (Self *copyBufferPool) Get() []byte {
74 | buf := Self.pool.Get().([]byte)
75 | return buf[:PoolSizeCopy] // just like make a new slice, but data may not be 0
76 | }
77 |
78 | func (Self *copyBufferPool) Put(x []byte) {
79 | if len(x) == PoolSizeCopy {
80 | Self.pool.Put(x)
81 | } else {
82 | x = nil // buf is not full, not allowed, New method returns a full buf
83 | }
84 | }
85 |
86 | var once = sync.Once{}
87 | var CopyBuff = copyBufferPool{}
88 |
89 | func newPool() {
90 | CopyBuff.New()
91 | }
92 |
93 | func init() {
94 | once.Do(newPool)
95 | }
96 |
--------------------------------------------------------------------------------
/lib/common/pprof.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "github.com/astaxie/beego"
5 | "github.com/astaxie/beego/logs"
6 | "net/http"
7 | _ "net/http/pprof"
8 | )
9 |
10 | func InitPProfFromFile() {
11 | ip := beego.AppConfig.String("pprof_ip")
12 | p := beego.AppConfig.String("pprof_port")
13 | if len(ip) > 0 && len(p) > 0 && IsPort(p) {
14 | runPProf(ip + ":" + p)
15 | }
16 | }
17 |
18 | func InitPProfFromArg(arg string) {
19 | if len(arg) > 0 {
20 | runPProf(arg)
21 | }
22 | }
23 |
24 | func runPProf(ipPort string) {
25 | go func() {
26 | _ = http.ListenAndServe(ipPort, nil)
27 | }()
28 | logs.Info("PProf debug listen on", ipPort)
29 | }
30 |
--------------------------------------------------------------------------------
/lib/common/run.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | "runtime"
7 | )
8 |
9 | //Get the currently selected configuration file directory
10 | //For non-Windows systems, select the /etc/nps as config directory if exist, or select ./
11 | //windows system, select the C:\Program Files\nps as config directory if exist, or select ./
12 | func GetRunPath() string {
13 | var path string
14 | if path = GetInstallPath(); !FileExists(path) {
15 | return GetAppPath()
16 | }
17 | return path
18 | }
19 |
20 | //Different systems get different installation paths
21 | func GetInstallPath() string {
22 | var path string
23 | if IsWindows() {
24 | path = `C:\Program Files\nps`
25 | } else {
26 | path = "/etc/nps"
27 | }
28 | return path
29 | }
30 |
31 | //Get the absolute path to the running directory
32 | func GetAppPath() string {
33 | if path, err := filepath.Abs(filepath.Dir(os.Args[0])); err == nil {
34 | return path
35 | }
36 | return os.Args[0]
37 | }
38 |
39 | //Determine whether the current system is a Windows system?
40 | func IsWindows() bool {
41 | if runtime.GOOS == "windows" {
42 | return true
43 | }
44 | return false
45 | }
46 |
47 | //interface log file path
48 | func GetLogPath() string {
49 | var path string
50 | if IsWindows() {
51 | path = filepath.Join(GetAppPath(), "nps.log")
52 | } else {
53 | path = "/var/log/nps.log"
54 | }
55 | return path
56 | }
57 |
58 | //interface npc log file path
59 | func GetNpcLogPath() string {
60 | var path string
61 | if IsWindows() {
62 | path = filepath.Join(GetAppPath(), "npc.log")
63 | } else {
64 | path = "/var/log/npc.log"
65 | }
66 | return path
67 | }
68 |
69 | //interface pid file path
70 | func GetTmpPath() string {
71 | var path string
72 | if IsWindows() {
73 | path = GetAppPath()
74 | } else {
75 | path = "/tmp"
76 | }
77 | return path
78 | }
79 |
80 | //config file path
81 | func GetConfigPath() string {
82 | var path string
83 | if IsWindows() {
84 | path = filepath.Join(GetAppPath(), "conf/npc.conf")
85 | } else {
86 | path = "conf/npc.conf"
87 | }
88 | return path
89 | }
90 |
--------------------------------------------------------------------------------
/lib/config/config_test.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "log"
5 | "regexp"
6 | "testing"
7 | )
8 |
9 | func TestReg(t *testing.T) {
10 | content := `
11 | [common]
12 | server=127.0.0.1:8284
13 | tp=tcp
14 | vkey=123
15 | [web2]
16 | host=www.baidu.com
17 | host_change=www.sina.com
18 | target=127.0.0.1:8080,127.0.0.1:8082
19 | header_cookkile=122123
20 | header_user-Agent=122123
21 | [web2]
22 | host=www.baidu.com
23 | host_change=www.sina.com
24 | target=127.0.0.1:8080,127.0.0.1:8082
25 | header_cookkile="122123"
26 | header_user-Agent=122123
27 | [tunnel1]
28 | type=udp
29 | target=127.0.0.1:8080
30 | port=9001
31 | compress=snappy
32 | crypt=true
33 | u=1
34 | p=2
35 | [tunnel2]
36 | type=tcp
37 | target=127.0.0.1:8080
38 | port=9001
39 | compress=snappy
40 | crypt=true
41 | u=1
42 | p=2
43 | `
44 | re, err := regexp.Compile(`\[.+?\]`)
45 | if err != nil {
46 | t.Fail()
47 | }
48 | log.Println(re.FindAllString(content, -1))
49 | }
50 |
51 | func TestDealCommon(t *testing.T) {
52 | s := `server=127.0.0.1:8284
53 | tp=tcp
54 | vkey=123`
55 | f := new(CommonConfig)
56 | f.Server = "127.0.0.1:8284"
57 | f.Tp = "tcp"
58 | f.VKey = "123"
59 | if c := dealCommon(s); *c != *f {
60 | t.Fail()
61 | }
62 | }
63 |
64 | func TestGetTitleContent(t *testing.T) {
65 | s := "[common]"
66 | if getTitleContent(s) != "common" {
67 | t.Fail()
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/lib/conn/link.go:
--------------------------------------------------------------------------------
1 | package conn
2 |
3 | import "time"
4 |
5 | type Secret struct {
6 | Password string
7 | Conn *Conn
8 | }
9 |
10 | func NewSecret(p string, conn *Conn) *Secret {
11 | return &Secret{
12 | Password: p,
13 | Conn: conn,
14 | }
15 | }
16 |
17 | type Link struct {
18 | ConnType string //连接类型
19 | Host string //目标
20 | Crypt bool //加密
21 | Compress bool
22 | LocalProxy bool
23 | RemoteAddr string
24 | Option Options
25 | }
26 |
27 | type Option func(*Options)
28 |
29 | type Options struct {
30 | Timeout time.Duration
31 | }
32 |
33 | var defaultTimeOut = time.Second * 5
34 |
35 | func NewLink(connType string, host string, crypt bool, compress bool, remoteAddr string, localProxy bool, opts ...Option) *Link {
36 | options := newOptions(opts...)
37 |
38 | return &Link{
39 | RemoteAddr: remoteAddr,
40 | ConnType: connType,
41 | Host: host,
42 | Crypt: crypt,
43 | Compress: compress,
44 | LocalProxy: localProxy,
45 | Option: options,
46 | }
47 | }
48 |
49 | func newOptions(opts ...Option) Options {
50 | opt := Options{
51 | Timeout: defaultTimeOut,
52 | }
53 | for _, o := range opts {
54 | o(&opt)
55 | }
56 | return opt
57 | }
58 |
59 | func LinkTimeout(t time.Duration) Option {
60 | return func(opt *Options) {
61 | opt.Timeout = t
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/lib/conn/listener.go:
--------------------------------------------------------------------------------
1 | package conn
2 |
3 | import (
4 | "net"
5 | "strings"
6 |
7 | "github.com/astaxie/beego/logs"
8 | "github.com/xtaci/kcp-go"
9 | )
10 |
11 | func NewTcpListenerAndProcess(addr string, f func(c net.Conn), listener *net.Listener) error {
12 | var err error
13 | *listener, err = net.Listen("tcp", addr)
14 | if err != nil {
15 | return err
16 | }
17 | Accept(*listener, f)
18 | return nil
19 | }
20 |
21 | func NewKcpListenerAndProcess(addr string, f func(c net.Conn)) error {
22 | kcpListener, err := kcp.ListenWithOptions(addr, nil, 150, 3)
23 | if err != nil {
24 | logs.Error(err)
25 | return err
26 | }
27 | for {
28 | c, err := kcpListener.AcceptKCP()
29 | SetUdpSession(c)
30 | if err != nil {
31 | logs.Warn(err)
32 | continue
33 | }
34 | go f(c)
35 | }
36 | return nil
37 | }
38 |
39 | func Accept(l net.Listener, f func(c net.Conn)) {
40 | for {
41 | c, err := l.Accept()
42 | if err != nil {
43 | if strings.Contains(err.Error(), "use of closed network connection") {
44 | break
45 | }
46 | if strings.Contains(err.Error(), "the mux has closed") {
47 | break
48 | }
49 | logs.Warn(err)
50 | continue
51 | }
52 | if c == nil {
53 | logs.Warn("nil connection")
54 | break
55 | }
56 | go f(c)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/lib/conn/snappy.go:
--------------------------------------------------------------------------------
1 | package conn
2 |
3 | import (
4 | "errors"
5 | "io"
6 |
7 | "github.com/golang/snappy"
8 | )
9 |
10 | type SnappyConn struct {
11 | w *snappy.Writer
12 | r *snappy.Reader
13 | c io.Closer
14 | }
15 |
16 | func NewSnappyConn(conn io.ReadWriteCloser) *SnappyConn {
17 | c := new(SnappyConn)
18 | c.w = snappy.NewBufferedWriter(conn)
19 | c.r = snappy.NewReader(conn)
20 | c.c = conn.(io.Closer)
21 | return c
22 | }
23 |
24 | //snappy压缩写
25 | func (s *SnappyConn) Write(b []byte) (n int, err error) {
26 | if n, err = s.w.Write(b); err != nil {
27 | return
28 | }
29 | if err = s.w.Flush(); err != nil {
30 | return
31 | }
32 | return
33 | }
34 |
35 | //snappy压缩读
36 | func (s *SnappyConn) Read(b []byte) (n int, err error) {
37 | return s.r.Read(b)
38 | }
39 |
40 | func (s *SnappyConn) Close() error {
41 | err := s.w.Close()
42 | err2 := s.c.Close()
43 | if err != nil && err2 == nil {
44 | return err
45 | }
46 | if err == nil && err2 != nil {
47 | return err2
48 | }
49 | if err != nil && err2 != nil {
50 | return errors.New(err.Error() + err2.Error())
51 | }
52 | return nil
53 | }
54 |
--------------------------------------------------------------------------------
/lib/crypt/crypt.go:
--------------------------------------------------------------------------------
1 | package crypt
2 |
3 | import (
4 | "bytes"
5 | "crypto/aes"
6 | "crypto/cipher"
7 | "crypto/md5"
8 | "encoding/hex"
9 | "errors"
10 | "math/rand"
11 | "time"
12 | )
13 |
14 | //en
15 | func AesEncrypt(origData, key []byte) ([]byte, error) {
16 | block, err := aes.NewCipher(key)
17 | if err != nil {
18 | return nil, err
19 | }
20 | blockSize := block.BlockSize()
21 | origData = PKCS5Padding(origData, blockSize)
22 | blockMode := cipher.NewCBCEncrypter(block, key[:blockSize])
23 | crypted := make([]byte, len(origData))
24 | blockMode.CryptBlocks(crypted, origData)
25 | return crypted, nil
26 | }
27 |
28 | //de
29 | func AesDecrypt(crypted, key []byte) ([]byte, error) {
30 | block, err := aes.NewCipher(key)
31 | if err != nil {
32 | return nil, err
33 | }
34 | blockSize := block.BlockSize()
35 | blockMode := cipher.NewCBCDecrypter(block, key[:blockSize])
36 | origData := make([]byte, len(crypted))
37 | blockMode.CryptBlocks(origData, crypted)
38 | err, origData = PKCS5UnPadding(origData)
39 | return origData, err
40 | }
41 |
42 | //Completion when the length is insufficient
43 | func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
44 | padding := blockSize - len(ciphertext)%blockSize
45 | padtext := bytes.Repeat([]byte{byte(padding)}, padding)
46 | return append(ciphertext, padtext...)
47 | }
48 |
49 | //Remove excess
50 | func PKCS5UnPadding(origData []byte) (error, []byte) {
51 | length := len(origData)
52 | unpadding := int(origData[length-1])
53 | if (length - unpadding) < 0 {
54 | return errors.New("len error"), nil
55 | }
56 | return nil, origData[:(length - unpadding)]
57 | }
58 |
59 | //Generate 32-bit MD5 strings
60 | func Md5(s string) string {
61 | h := md5.New()
62 | h.Write([]byte(s))
63 | return hex.EncodeToString(h.Sum(nil))
64 | }
65 |
66 | //Generating Random Verification Key
67 | func GetRandomString(l int) string {
68 | str := "0123456789abcdefghijklmnopqrstuvwxyz"
69 | bytes := []byte(str)
70 | result := []byte{}
71 | r := rand.New(rand.NewSource(time.Now().UnixNano()))
72 | for i := 0; i < l; i++ {
73 | result = append(result, bytes[r.Intn(len(bytes))])
74 | }
75 | return string(result)
76 | }
77 |
--------------------------------------------------------------------------------
/lib/crypt/tls.go:
--------------------------------------------------------------------------------
1 | package crypt
2 |
3 | import (
4 | "crypto/rand"
5 | "crypto/rsa"
6 | "crypto/tls"
7 | "crypto/x509"
8 | "crypto/x509/pkix"
9 | "encoding/pem"
10 | "log"
11 | "math/big"
12 | "net"
13 | "os"
14 | "time"
15 |
16 | "github.com/astaxie/beego/logs"
17 | )
18 |
19 | var (
20 | cert tls.Certificate
21 | )
22 |
23 | func InitTls() {
24 | c, k, err := generateKeyPair("NPS Org")
25 | if err == nil {
26 | cert, err = tls.X509KeyPair(c, k)
27 | }
28 | if err != nil {
29 | log.Fatalln("Error initializing crypto certs", err)
30 | }
31 | }
32 |
33 | func NewTlsServerConn(conn net.Conn) net.Conn {
34 | var err error
35 | if err != nil {
36 | logs.Error(err)
37 | os.Exit(0)
38 | return nil
39 | }
40 | config := &tls.Config{Certificates: []tls.Certificate{cert}}
41 | return tls.Server(conn, config)
42 | }
43 |
44 | func NewTlsClientConn(conn net.Conn) net.Conn {
45 | conf := &tls.Config{
46 | InsecureSkipVerify: true,
47 | }
48 | return tls.Client(conn, conf)
49 | }
50 |
51 | func generateKeyPair(CommonName string) (rawCert, rawKey []byte, err error) {
52 | // Create private key and self-signed certificate
53 | // Adapted from https://golang.org/src/crypto/tls/generate_cert.go
54 |
55 | priv, err := rsa.GenerateKey(rand.Reader, 2048)
56 | if err != nil {
57 | return
58 | }
59 | validFor := time.Hour * 24 * 365 * 10 // ten years
60 | notBefore := time.Now()
61 | notAfter := notBefore.Add(validFor)
62 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
63 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
64 | template := x509.Certificate{
65 | SerialNumber: serialNumber,
66 | Subject: pkix.Name{
67 | Organization: []string{"My Company Name LTD."},
68 | CommonName: CommonName,
69 | Country: []string{"US"},
70 | },
71 | NotBefore: notBefore,
72 | NotAfter: notAfter,
73 |
74 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
75 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
76 | BasicConstraintsValid: true,
77 | }
78 | derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
79 | if err != nil {
80 | return
81 | }
82 |
83 | rawCert = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
84 | rawKey = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
85 |
86 | return
87 | }
88 |
--------------------------------------------------------------------------------
/lib/daemon/daemon.go:
--------------------------------------------------------------------------------
1 | package daemon
2 |
3 | import (
4 | "io/ioutil"
5 | "log"
6 | "os"
7 | "os/exec"
8 | "path/filepath"
9 | "strconv"
10 | "strings"
11 |
12 | "ehang.io/nps/lib/common"
13 | )
14 |
15 | func InitDaemon(f string, runPath string, pidPath string) {
16 | if len(os.Args) < 2 {
17 | return
18 | }
19 | var args []string
20 | args = append(args, os.Args[0])
21 | if len(os.Args) >= 2 {
22 | args = append(args, os.Args[2:]...)
23 | }
24 | args = append(args, "-log=file")
25 | switch os.Args[1] {
26 | case "start":
27 | start(args, f, pidPath, runPath)
28 | os.Exit(0)
29 | case "stop":
30 | stop(f, args[0], pidPath)
31 | os.Exit(0)
32 | case "restart":
33 | stop(f, args[0], pidPath)
34 | start(args, f, pidPath, runPath)
35 | os.Exit(0)
36 | case "status":
37 | if status(f, pidPath) {
38 | log.Printf("%s is running", f)
39 | } else {
40 | log.Printf("%s is not running", f)
41 | }
42 | os.Exit(0)
43 | case "reload":
44 | reload(f, pidPath)
45 | os.Exit(0)
46 | }
47 | }
48 |
49 | func reload(f string, pidPath string) {
50 | if f == "nps" && !common.IsWindows() && !status(f, pidPath) {
51 | log.Println("reload fail")
52 | return
53 | }
54 | var c *exec.Cmd
55 | var err error
56 | b, err := ioutil.ReadFile(filepath.Join(pidPath, f+".pid"))
57 | if err == nil {
58 | c = exec.Command("/bin/bash", "-c", `kill -30 `+string(b))
59 | } else {
60 | log.Fatalln("reload error,pid file does not exist")
61 | }
62 | if c.Run() == nil {
63 | log.Println("reload success")
64 | } else {
65 | log.Println("reload fail")
66 | }
67 | }
68 |
69 | func status(f string, pidPath string) bool {
70 | var cmd *exec.Cmd
71 | b, err := ioutil.ReadFile(filepath.Join(pidPath, f+".pid"))
72 | if err == nil {
73 | if !common.IsWindows() {
74 | cmd = exec.Command("/bin/sh", "-c", "ps -ax | awk '{ print $1 }' | grep "+string(b))
75 | } else {
76 | cmd = exec.Command("tasklist")
77 | }
78 | out, _ := cmd.Output()
79 | if strings.Index(string(out), string(b)) > -1 {
80 | return true
81 | }
82 | }
83 | return false
84 | }
85 |
86 | func start(osArgs []string, f string, pidPath, runPath string) {
87 | if status(f, pidPath) {
88 | log.Printf(" %s is running", f)
89 | return
90 | }
91 | cmd := exec.Command(osArgs[0], osArgs[1:]...)
92 | cmd.Start()
93 | if cmd.Process.Pid > 0 {
94 | log.Println("start ok , pid:", cmd.Process.Pid, "config path:", runPath)
95 | d1 := []byte(strconv.Itoa(cmd.Process.Pid))
96 | ioutil.WriteFile(filepath.Join(pidPath, f+".pid"), d1, 0600)
97 | } else {
98 | log.Println("start error")
99 | }
100 | }
101 |
102 | func stop(f string, p string, pidPath string) {
103 | if !status(f, pidPath) {
104 | log.Printf(" %s is not running", f)
105 | return
106 | }
107 | var c *exec.Cmd
108 | var err error
109 | if common.IsWindows() {
110 | p := strings.Split(p, `\`)
111 | c = exec.Command("taskkill", "/F", "/IM", p[len(p)-1])
112 | } else {
113 | b, err := ioutil.ReadFile(filepath.Join(pidPath, f+".pid"))
114 | if err == nil {
115 | c = exec.Command("/bin/bash", "-c", `kill -9 `+string(b))
116 | } else {
117 | log.Fatalln("stop error,pid file does not exist")
118 | }
119 | }
120 | err = c.Run()
121 | if err != nil {
122 | log.Println("stop error,", err)
123 | } else {
124 | log.Println("stop ok")
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/lib/daemon/reload.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package daemon
4 |
5 | import (
6 | "os"
7 | "os/signal"
8 | "path/filepath"
9 | "syscall"
10 |
11 | "ehang.io/nps/lib/common"
12 | "github.com/astaxie/beego"
13 | )
14 |
15 | func init() {
16 | s := make(chan os.Signal, 1)
17 | signal.Notify(s, syscall.SIGUSR1)
18 | go func() {
19 | for {
20 | <-s
21 | beego.LoadAppConfig("ini", filepath.Join(common.GetRunPath(), "conf", "nps.conf"))
22 | }
23 | }()
24 | }
25 |
--------------------------------------------------------------------------------
/lib/file/file.go:
--------------------------------------------------------------------------------
1 | package file
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "github.com/astaxie/beego/logs"
7 | "os"
8 | "path/filepath"
9 | "strings"
10 | "sync"
11 | "sync/atomic"
12 |
13 | "ehang.io/nps/lib/common"
14 | "ehang.io/nps/lib/rate"
15 | )
16 |
17 | func NewJsonDb(runPath string) *JsonDb {
18 | return &JsonDb{
19 | RunPath: runPath,
20 | TaskFilePath: filepath.Join(runPath, "conf", "tasks.json"),
21 | HostFilePath: filepath.Join(runPath, "conf", "hosts.json"),
22 | ClientFilePath: filepath.Join(runPath, "conf", "clients.json"),
23 | }
24 | }
25 |
26 | type JsonDb struct {
27 | Tasks sync.Map
28 | Hosts sync.Map
29 | HostsTmp sync.Map
30 | Clients sync.Map
31 | RunPath string
32 | ClientIncreaseId int32 //client increased id
33 | TaskIncreaseId int32 //task increased id
34 | HostIncreaseId int32 //host increased id
35 | TaskFilePath string //task file path
36 | HostFilePath string //host file path
37 | ClientFilePath string //client file path
38 | }
39 |
40 | func (s *JsonDb) LoadTaskFromJsonFile() {
41 | loadSyncMapFromFile(s.TaskFilePath, func(v string) {
42 | var err error
43 | post := new(Tunnel)
44 | if json.Unmarshal([]byte(v), &post) != nil {
45 | return
46 | }
47 | if post.Client, err = s.GetClient(post.Client.Id); err != nil {
48 | return
49 | }
50 | s.Tasks.Store(post.Id, post)
51 | if post.Id > int(s.TaskIncreaseId) {
52 | s.TaskIncreaseId = int32(post.Id)
53 | }
54 | })
55 | }
56 |
57 | func (s *JsonDb) LoadClientFromJsonFile() {
58 | loadSyncMapFromFile(s.ClientFilePath, func(v string) {
59 | post := new(Client)
60 | if json.Unmarshal([]byte(v), &post) != nil {
61 | return
62 | }
63 | if post.RateLimit > 0 {
64 | post.Rate = rate.NewRate(int64(post.RateLimit * 1024))
65 | } else {
66 | post.Rate = rate.NewRate(int64(2 << 23))
67 | }
68 | post.Rate.Start()
69 | post.NowConn = 0
70 | s.Clients.Store(post.Id, post)
71 | if post.Id > int(s.ClientIncreaseId) {
72 | s.ClientIncreaseId = int32(post.Id)
73 | }
74 | })
75 | }
76 |
77 | func (s *JsonDb) LoadHostFromJsonFile() {
78 | loadSyncMapFromFile(s.HostFilePath, func(v string) {
79 | var err error
80 | post := new(Host)
81 | if json.Unmarshal([]byte(v), &post) != nil {
82 | return
83 | }
84 | if post.Client, err = s.GetClient(post.Client.Id); err != nil {
85 | return
86 | }
87 | s.Hosts.Store(post.Id, post)
88 | if post.Id > int(s.HostIncreaseId) {
89 | s.HostIncreaseId = int32(post.Id)
90 | }
91 | })
92 | }
93 |
94 | func (s *JsonDb) GetClient(id int) (c *Client, err error) {
95 | if v, ok := s.Clients.Load(id); ok {
96 | c = v.(*Client)
97 | return
98 | }
99 | err = errors.New("未找到客户端")
100 | return
101 | }
102 |
103 | var hostLock sync.Mutex
104 |
105 | func (s *JsonDb) StoreHostToJsonFile() {
106 | hostLock.Lock()
107 | storeSyncMapToFile(s.Hosts, s.HostFilePath)
108 | hostLock.Unlock()
109 | }
110 |
111 | var taskLock sync.Mutex
112 |
113 | func (s *JsonDb) StoreTasksToJsonFile() {
114 | taskLock.Lock()
115 | storeSyncMapToFile(s.Tasks, s.TaskFilePath)
116 | taskLock.Unlock()
117 | }
118 |
119 | var clientLock sync.Mutex
120 |
121 | func (s *JsonDb) StoreClientsToJsonFile() {
122 | clientLock.Lock()
123 | storeSyncMapToFile(s.Clients, s.ClientFilePath)
124 | clientLock.Unlock()
125 | }
126 |
127 | func (s *JsonDb) GetClientId() int32 {
128 | return atomic.AddInt32(&s.ClientIncreaseId, 1)
129 | }
130 |
131 | func (s *JsonDb) GetTaskId() int32 {
132 | return atomic.AddInt32(&s.TaskIncreaseId, 1)
133 | }
134 |
135 | func (s *JsonDb) GetHostId() int32 {
136 | return atomic.AddInt32(&s.HostIncreaseId, 1)
137 | }
138 |
139 | func loadSyncMapFromFile(filePath string, f func(value string)) {
140 | b, err := common.ReadAllFromFile(filePath)
141 | if err != nil {
142 | panic(err)
143 | }
144 | for _, v := range strings.Split(string(b), "\n"+common.CONN_DATA_SEQ) {
145 | f(v)
146 | }
147 | }
148 |
149 | func storeSyncMapToFile(m sync.Map, filePath string) {
150 | file, err := os.Create(filePath + ".tmp")
151 | // first create a temporary file to store
152 | if err != nil {
153 | panic(err)
154 | }
155 | m.Range(func(key, value interface{}) bool {
156 | var b []byte
157 | var err error
158 | switch value.(type) {
159 | case *Tunnel:
160 | obj := value.(*Tunnel)
161 | if obj.NoStore {
162 | return true
163 | }
164 | b, err = json.Marshal(obj)
165 | case *Host:
166 | obj := value.(*Host)
167 | if obj.NoStore {
168 | return true
169 | }
170 | b, err = json.Marshal(obj)
171 | case *Client:
172 | obj := value.(*Client)
173 | if obj.NoStore {
174 | return true
175 | }
176 | b, err = json.Marshal(obj)
177 | default:
178 | return true
179 | }
180 | if err != nil {
181 | return true
182 | }
183 | _, err = file.Write(b)
184 | if err != nil {
185 | panic(err)
186 | }
187 | _, err = file.Write([]byte("\n" + common.CONN_DATA_SEQ))
188 | if err != nil {
189 | panic(err)
190 | }
191 | return true
192 | })
193 | _ = file.Sync()
194 | _ = file.Close()
195 | // must close file first, then rename it
196 | err = os.Rename(filePath+".tmp", filePath)
197 | if err != nil {
198 | logs.Error(err, "store to file err, data will lost")
199 | }
200 | // replace the file, maybe provides atomic operation
201 | }
202 |
--------------------------------------------------------------------------------
/lib/file/obj.go:
--------------------------------------------------------------------------------
1 | package file
2 |
3 | import (
4 | "strings"
5 | "sync"
6 | "sync/atomic"
7 | "time"
8 |
9 | "ehang.io/nps/lib/rate"
10 | "github.com/pkg/errors"
11 | )
12 |
13 | type Flow struct {
14 | ExportFlow int64
15 | InletFlow int64
16 | FlowLimit int64
17 | sync.RWMutex
18 | }
19 |
20 | func (s *Flow) Add(in, out int64) {
21 | s.Lock()
22 | defer s.Unlock()
23 | s.InletFlow += int64(in)
24 | s.ExportFlow += int64(out)
25 | }
26 |
27 | type Config struct {
28 | U string
29 | P string
30 | Compress bool
31 | Crypt bool
32 | }
33 |
34 | type Client struct {
35 | Cnf *Config
36 | Id int //id
37 | VerifyKey string //verify key
38 | Addr string //the ip of client
39 | Remark string //remark
40 | Status bool //is allow connect
41 | IsConnect bool //is the client connect
42 | RateLimit int //rate /kb
43 | Flow *Flow //flow setting
44 | Rate *rate.Rate //rate limit
45 | NoStore bool //no store to file
46 | NoDisplay bool //no display on web
47 | MaxConn int //the max connection num of client allow
48 | NowConn int32 //the connection num of now
49 | WebUserName string //the username of web login
50 | WebPassword string //the password of web login
51 | ConfigConnAllow bool //is allow connected by config file
52 | MaxTunnelNum int
53 | Version string
54 | sync.RWMutex
55 | }
56 |
57 | func NewClient(vKey string, noStore bool, noDisplay bool) *Client {
58 | return &Client{
59 | Cnf: new(Config),
60 | Id: 0,
61 | VerifyKey: vKey,
62 | Addr: "",
63 | Remark: "",
64 | Status: true,
65 | IsConnect: false,
66 | RateLimit: 0,
67 | Flow: new(Flow),
68 | Rate: nil,
69 | NoStore: noStore,
70 | RWMutex: sync.RWMutex{},
71 | NoDisplay: noDisplay,
72 | }
73 | }
74 |
75 | func (s *Client) CutConn() {
76 | atomic.AddInt32(&s.NowConn, 1)
77 | }
78 |
79 | func (s *Client) AddConn() {
80 | atomic.AddInt32(&s.NowConn, -1)
81 | }
82 |
83 | func (s *Client) GetConn() bool {
84 | if s.MaxConn == 0 || int(s.NowConn) < s.MaxConn {
85 | s.CutConn()
86 | return true
87 | }
88 | return false
89 | }
90 |
91 | func (s *Client) HasTunnel(t *Tunnel) (exist bool) {
92 | GetDb().JsonDb.Tasks.Range(func(key, value interface{}) bool {
93 | v := value.(*Tunnel)
94 | if v.Client.Id == s.Id && v.Port == t.Port && t.Port != 0 {
95 | exist = true
96 | return false
97 | }
98 | return true
99 | })
100 | return
101 | }
102 |
103 | func (s *Client) GetTunnelNum() (num int) {
104 | GetDb().JsonDb.Tasks.Range(func(key, value interface{}) bool {
105 | v := value.(*Tunnel)
106 | if v.Client.Id == s.Id {
107 | num++
108 | }
109 | return true
110 | })
111 | return
112 | }
113 |
114 | func (s *Client) HasHost(h *Host) bool {
115 | var has bool
116 | GetDb().JsonDb.Hosts.Range(func(key, value interface{}) bool {
117 | v := value.(*Host)
118 | if v.Client.Id == s.Id && v.Host == h.Host && h.Location == v.Location {
119 | has = true
120 | return false
121 | }
122 | return true
123 | })
124 | return has
125 | }
126 |
127 | type Tunnel struct {
128 | Id int
129 | Port int
130 | ServerIp string
131 | Mode string
132 | Status bool
133 | RunStatus bool
134 | Client *Client
135 | Ports string
136 | Flow *Flow
137 | Password string
138 | Remark string
139 | TargetAddr string
140 | NoStore bool
141 | LocalPath string
142 | StripPre string
143 | Target *Target
144 | MultiAccount *MultiAccount
145 | Health
146 | sync.RWMutex
147 | }
148 |
149 | type Health struct {
150 | HealthCheckTimeout int
151 | HealthMaxFail int
152 | HealthCheckInterval int
153 | HealthNextTime time.Time
154 | HealthMap map[string]int
155 | HttpHealthUrl string
156 | HealthRemoveArr []string
157 | HealthCheckType string
158 | HealthCheckTarget string
159 | sync.RWMutex
160 | }
161 |
162 | type Host struct {
163 | Id int
164 | Host string //host
165 | HeaderChange string //header change
166 | HostChange string //host change
167 | Location string //url router
168 | Remark string //remark
169 | Scheme string //http https all
170 | CertFilePath string
171 | KeyFilePath string
172 | NoStore bool
173 | IsClose bool
174 | Flow *Flow
175 | Client *Client
176 | Target *Target //目标
177 | Health `json:"-"`
178 | sync.RWMutex
179 | }
180 |
181 | type Target struct {
182 | nowIndex int
183 | TargetStr string
184 | TargetArr []string
185 | LocalProxy bool
186 | sync.RWMutex
187 | }
188 |
189 | type MultiAccount struct {
190 | AccountMap map[string]string // multi account and pwd
191 | }
192 |
193 | func (s *Target) GetRandomTarget() (string, error) {
194 | if s.TargetArr == nil {
195 | s.TargetArr = strings.Split(s.TargetStr, "\n")
196 | }
197 | if len(s.TargetArr) == 1 {
198 | return s.TargetArr[0], nil
199 | }
200 | if len(s.TargetArr) == 0 {
201 | return "", errors.New("all inward-bending targets are offline")
202 | }
203 | s.Lock()
204 | defer s.Unlock()
205 | if s.nowIndex >= len(s.TargetArr)-1 {
206 | s.nowIndex = -1
207 | }
208 | s.nowIndex++
209 | return s.TargetArr[s.nowIndex], nil
210 | }
211 |
--------------------------------------------------------------------------------
/lib/file/sort.go:
--------------------------------------------------------------------------------
1 | package file
2 |
3 | import (
4 | "reflect"
5 | "sort"
6 | "sync"
7 | )
8 |
9 | // A data structure to hold a key/value pair.
10 | type Pair struct {
11 | key string //sort key
12 | cId int
13 | order string
14 | clientFlow *Flow
15 | }
16 |
17 | // A slice of Pairs that implements sort.Interface to sort by Value.
18 | type PairList []*Pair
19 |
20 | func (p PairList) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
21 | func (p PairList) Len() int { return len(p) }
22 | func (p PairList) Less(i, j int) bool {
23 | if p[i].order == "desc" {
24 | return reflect.ValueOf(*p[i].clientFlow).FieldByName(p[i].key).Int() < reflect.ValueOf(*p[j].clientFlow).FieldByName(p[j].key).Int()
25 | }
26 | return reflect.ValueOf(*p[i].clientFlow).FieldByName(p[i].key).Int() > reflect.ValueOf(*p[j].clientFlow).FieldByName(p[j].key).Int()
27 | }
28 |
29 | // A function to turn a map into a PairList, then sort and return it.
30 | func sortClientByKey(m sync.Map, sortKey, order string) (res []int) {
31 | p := make(PairList, 0)
32 | m.Range(func(key, value interface{}) bool {
33 | p = append(p, &Pair{sortKey, value.(*Client).Id, order, value.(*Client).Flow})
34 | return true
35 | })
36 | sort.Sort(p)
37 | for _, v := range p {
38 | res = append(res, v.cId)
39 | }
40 | return
41 | }
42 |
--------------------------------------------------------------------------------
/lib/goroutine/pool.go:
--------------------------------------------------------------------------------
1 | package goroutine
2 |
3 | import (
4 | "ehang.io/nps/lib/common"
5 | "ehang.io/nps/lib/file"
6 | "github.com/panjf2000/ants/v2"
7 | "io"
8 | "net"
9 | "sync"
10 | )
11 |
12 | type connGroup struct {
13 | src io.ReadWriteCloser
14 | dst io.ReadWriteCloser
15 | wg *sync.WaitGroup
16 | n *int64
17 | }
18 |
19 | func newConnGroup(dst, src io.ReadWriteCloser, wg *sync.WaitGroup, n *int64) connGroup {
20 | return connGroup{
21 | src: src,
22 | dst: dst,
23 | wg: wg,
24 | n: n,
25 | }
26 | }
27 |
28 | func copyConnGroup(group interface{}) {
29 | cg, ok := group.(connGroup)
30 | if !ok {
31 | return
32 | }
33 | var err error
34 | *cg.n, err = common.CopyBuffer(cg.dst, cg.src)
35 | if err != nil {
36 | cg.src.Close()
37 | cg.dst.Close()
38 | //logs.Warn("close npc by copy from nps", err, c.connId)
39 | }
40 | cg.wg.Done()
41 | }
42 |
43 | type Conns struct {
44 | conn1 io.ReadWriteCloser // mux connection
45 | conn2 net.Conn // outside connection
46 | flow *file.Flow
47 | wg *sync.WaitGroup
48 | }
49 |
50 | func NewConns(c1 io.ReadWriteCloser, c2 net.Conn, flow *file.Flow, wg *sync.WaitGroup) Conns {
51 | return Conns{
52 | conn1: c1,
53 | conn2: c2,
54 | flow: flow,
55 | wg: wg,
56 | }
57 | }
58 |
59 | func copyConns(group interface{}) {
60 | conns := group.(Conns)
61 | wg := new(sync.WaitGroup)
62 | wg.Add(2)
63 | var in, out int64
64 | _ = connCopyPool.Invoke(newConnGroup(conns.conn1, conns.conn2, wg, &in))
65 | // outside to mux : incoming
66 | _ = connCopyPool.Invoke(newConnGroup(conns.conn2, conns.conn1, wg, &out))
67 | // mux to outside : outgoing
68 | wg.Wait()
69 | if conns.flow != nil {
70 | conns.flow.Add(in, out)
71 | }
72 | conns.wg.Done()
73 | }
74 |
75 | var connCopyPool, _ = ants.NewPoolWithFunc(200000, copyConnGroup, ants.WithNonblocking(false))
76 | var CopyConnsPool, _ = ants.NewPoolWithFunc(100000, copyConns, ants.WithNonblocking(false))
77 |
--------------------------------------------------------------------------------
/lib/pmux/pconn.go:
--------------------------------------------------------------------------------
1 | package pmux
2 |
3 | import (
4 | "net"
5 | "time"
6 | )
7 |
8 | type PortConn struct {
9 | Conn net.Conn
10 | rs []byte
11 | readMore bool
12 | start int
13 | }
14 |
15 | func newPortConn(conn net.Conn, rs []byte, readMore bool) *PortConn {
16 | return &PortConn{
17 | Conn: conn,
18 | rs: rs,
19 | readMore: readMore,
20 | }
21 | }
22 |
23 | func (pConn *PortConn) Read(b []byte) (n int, err error) {
24 | if len(b) < len(pConn.rs)-pConn.start {
25 | defer func() {
26 | pConn.start = pConn.start + len(b)
27 | }()
28 | return copy(b, pConn.rs), nil
29 | }
30 | if pConn.start < len(pConn.rs) {
31 | defer func() {
32 | pConn.start = len(pConn.rs)
33 | }()
34 | n = copy(b, pConn.rs[pConn.start:])
35 | if !pConn.readMore {
36 | return
37 | }
38 | }
39 | var n2 = 0
40 | n2, err = pConn.Conn.Read(b[n:])
41 | n = n + n2
42 | return
43 | }
44 |
45 | func (pConn *PortConn) Write(b []byte) (n int, err error) {
46 | return pConn.Conn.Write(b)
47 | }
48 |
49 | func (pConn *PortConn) Close() error {
50 | return pConn.Conn.Close()
51 | }
52 |
53 | func (pConn *PortConn) LocalAddr() net.Addr {
54 | return pConn.Conn.LocalAddr()
55 | }
56 |
57 | func (pConn *PortConn) RemoteAddr() net.Addr {
58 | return pConn.Conn.RemoteAddr()
59 | }
60 |
61 | func (pConn *PortConn) SetDeadline(t time.Time) error {
62 | return pConn.Conn.SetDeadline(t)
63 | }
64 |
65 | func (pConn *PortConn) SetReadDeadline(t time.Time) error {
66 | return pConn.Conn.SetReadDeadline(t)
67 | }
68 |
69 | func (pConn *PortConn) SetWriteDeadline(t time.Time) error {
70 | return pConn.Conn.SetWriteDeadline(t)
71 | }
72 |
--------------------------------------------------------------------------------
/lib/pmux/plistener.go:
--------------------------------------------------------------------------------
1 | package pmux
2 |
3 | import (
4 | "errors"
5 | "net"
6 | )
7 |
8 | type PortListener struct {
9 | net.Listener
10 | connCh chan *PortConn
11 | addr net.Addr
12 | isClose bool
13 | }
14 |
15 | func NewPortListener(connCh chan *PortConn, addr net.Addr) *PortListener {
16 | return &PortListener{
17 | connCh: connCh,
18 | addr: addr,
19 | }
20 | }
21 |
22 | func (pListener *PortListener) Accept() (net.Conn, error) {
23 | if pListener.isClose {
24 | return nil, errors.New("the listener has closed")
25 | }
26 | conn := <-pListener.connCh
27 | if conn != nil {
28 | return conn, nil
29 | }
30 | return nil, errors.New("the listener has closed")
31 | }
32 |
33 | func (pListener *PortListener) Close() error {
34 | //close
35 | if pListener.isClose {
36 | return errors.New("the listener has closed")
37 | }
38 | pListener.isClose = true
39 | return nil
40 | }
41 |
42 | func (pListener *PortListener) Addr() net.Addr {
43 | return pListener.addr
44 | }
45 |
--------------------------------------------------------------------------------
/lib/pmux/pmux.go:
--------------------------------------------------------------------------------
1 | // This module is used for port reuse
2 | // Distinguish client, web manager , HTTP and HTTPS according to the difference of protocol
3 | package pmux
4 |
5 | import (
6 | "bufio"
7 | "bytes"
8 | "io"
9 | "net"
10 | "os"
11 | "strconv"
12 | "strings"
13 | "time"
14 |
15 | "ehang.io/nps/lib/common"
16 | "github.com/astaxie/beego/logs"
17 | "github.com/pkg/errors"
18 | )
19 |
20 | const (
21 | HTTP_GET = 716984
22 | HTTP_POST = 807983
23 | HTTP_HEAD = 726965
24 | HTTP_PUT = 808585
25 | HTTP_DELETE = 686976
26 | HTTP_CONNECT = 677978
27 | HTTP_OPTIONS = 798084
28 | HTTP_TRACE = 848265
29 | CLIENT = 848384
30 | ACCEPT_TIME_OUT = 10
31 | )
32 |
33 | type PortMux struct {
34 | net.Listener
35 | port int
36 | isClose bool
37 | managerHost string
38 | clientConn chan *PortConn
39 | httpConn chan *PortConn
40 | httpsConn chan *PortConn
41 | managerConn chan *PortConn
42 | }
43 |
44 | func NewPortMux(port int, managerHost string) *PortMux {
45 | pMux := &PortMux{
46 | managerHost: managerHost,
47 | port: port,
48 | clientConn: make(chan *PortConn),
49 | httpConn: make(chan *PortConn),
50 | httpsConn: make(chan *PortConn),
51 | managerConn: make(chan *PortConn),
52 | }
53 | pMux.Start()
54 | return pMux
55 | }
56 |
57 | func (pMux *PortMux) Start() error {
58 | // Port multiplexing is based on TCP only
59 | tcpAddr, err := net.ResolveTCPAddr("tcp", "0.0.0.0:"+strconv.Itoa(pMux.port))
60 | if err != nil {
61 | return err
62 | }
63 | pMux.Listener, err = net.ListenTCP("tcp", tcpAddr)
64 | if err != nil {
65 | logs.Error(err)
66 | os.Exit(0)
67 | }
68 | go func() {
69 | for {
70 | conn, err := pMux.Listener.Accept()
71 | if err != nil {
72 | logs.Warn(err)
73 | //close
74 | pMux.Close()
75 | }
76 | go pMux.process(conn)
77 | }
78 | }()
79 | return nil
80 | }
81 |
82 | func (pMux *PortMux) process(conn net.Conn) {
83 | // Recognition according to different signs
84 | // read 3 byte
85 | buf := make([]byte, 3)
86 | if n, err := io.ReadFull(conn, buf); err != nil || n != 3 {
87 | return
88 | }
89 | var ch chan *PortConn
90 | var rs []byte
91 | var buffer bytes.Buffer
92 | var readMore = false
93 | switch common.BytesToNum(buf) {
94 | case HTTP_CONNECT, HTTP_DELETE, HTTP_GET, HTTP_HEAD, HTTP_OPTIONS, HTTP_POST, HTTP_PUT, HTTP_TRACE: //http and manager
95 | buffer.Reset()
96 | r := bufio.NewReader(conn)
97 | buffer.Write(buf)
98 | for {
99 | b, _, err := r.ReadLine()
100 | if err != nil {
101 | logs.Warn("read line error", err.Error())
102 | conn.Close()
103 | break
104 | }
105 | buffer.Write(b)
106 | buffer.Write([]byte("\r\n"))
107 | if strings.Index(string(b), "Host:") == 0 || strings.Index(string(b), "host:") == 0 {
108 | // Remove host and space effects
109 | str := strings.Replace(string(b), "Host:", "", -1)
110 | str = strings.Replace(str, "host:", "", -1)
111 | str = strings.TrimSpace(str)
112 | // Determine whether it is the same as the manager domain name
113 | if common.GetIpByAddr(str) == pMux.managerHost {
114 | ch = pMux.managerConn
115 | } else {
116 | ch = pMux.httpConn
117 | }
118 | b, _ := r.Peek(r.Buffered())
119 | buffer.Write(b)
120 | rs = buffer.Bytes()
121 | break
122 | }
123 | }
124 | case CLIENT: // client connection
125 | ch = pMux.clientConn
126 | default: // https
127 | readMore = true
128 | ch = pMux.httpsConn
129 | }
130 | if len(rs) == 0 {
131 | rs = buf
132 | }
133 | timer := time.NewTimer(ACCEPT_TIME_OUT)
134 | select {
135 | case <-timer.C:
136 | case ch <- newPortConn(conn, rs, readMore):
137 | }
138 | }
139 |
140 | func (pMux *PortMux) Close() error {
141 | if pMux.isClose {
142 | return errors.New("the port pmux has closed")
143 | }
144 | pMux.isClose = true
145 | close(pMux.clientConn)
146 | close(pMux.httpsConn)
147 | close(pMux.httpConn)
148 | close(pMux.managerConn)
149 | return pMux.Listener.Close()
150 | }
151 |
152 | func (pMux *PortMux) GetClientListener() net.Listener {
153 | return NewPortListener(pMux.clientConn, pMux.Listener.Addr())
154 | }
155 |
156 | func (pMux *PortMux) GetHttpListener() net.Listener {
157 | return NewPortListener(pMux.httpConn, pMux.Listener.Addr())
158 | }
159 |
160 | func (pMux *PortMux) GetHttpsListener() net.Listener {
161 | return NewPortListener(pMux.httpsConn, pMux.Listener.Addr())
162 | }
163 |
164 | func (pMux *PortMux) GetManagerListener() net.Listener {
165 | return NewPortListener(pMux.managerConn, pMux.Listener.Addr())
166 | }
167 |
--------------------------------------------------------------------------------
/lib/pmux/pmux_test.go:
--------------------------------------------------------------------------------
1 | package pmux
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/astaxie/beego/logs"
8 | )
9 |
10 | func TestPortMux_Close(t *testing.T) {
11 | logs.Reset()
12 | logs.EnableFuncCallDepth(true)
13 | logs.SetLogFuncCallDepth(3)
14 |
15 | pMux := NewPortMux(8888, "Ds")
16 | go func() {
17 | if pMux.Start() != nil {
18 | logs.Warn("Error")
19 | }
20 | }()
21 | time.Sleep(time.Second * 3)
22 | go func() {
23 | l := pMux.GetHttpListener()
24 | conn, err := l.Accept()
25 | logs.Warn(conn, err)
26 | }()
27 | go func() {
28 | l := pMux.GetHttpListener()
29 | conn, err := l.Accept()
30 | logs.Warn(conn, err)
31 | }()
32 | go func() {
33 | l := pMux.GetHttpListener()
34 | conn, err := l.Accept()
35 | logs.Warn(conn, err)
36 | }()
37 | l := pMux.GetHttpListener()
38 | conn, err := l.Accept()
39 | logs.Warn(conn, err)
40 | }
41 |
--------------------------------------------------------------------------------
/lib/rate/conn.go:
--------------------------------------------------------------------------------
1 | package rate
2 |
3 | import (
4 | "io"
5 | )
6 |
7 | type rateConn struct {
8 | conn io.ReadWriteCloser
9 | rate *Rate
10 | }
11 |
12 | func NewRateConn(conn io.ReadWriteCloser, rate *Rate) io.ReadWriteCloser {
13 | return &rateConn{
14 | conn: conn,
15 | rate: rate,
16 | }
17 | }
18 |
19 | func (s *rateConn) Read(b []byte) (n int, err error) {
20 | n, err = s.conn.Read(b)
21 | if s.rate != nil {
22 | s.rate.Get(int64(n))
23 | }
24 | return
25 | }
26 |
27 | func (s *rateConn) Write(b []byte) (n int, err error) {
28 | n, err = s.conn.Write(b)
29 | if s.rate != nil {
30 | s.rate.Get(int64(n))
31 | }
32 | return
33 | }
34 |
35 | func (s *rateConn) Close() error {
36 | return s.conn.Close()
37 | }
38 |
--------------------------------------------------------------------------------
/lib/rate/rate.go:
--------------------------------------------------------------------------------
1 | package rate
2 |
3 | import (
4 | "sync/atomic"
5 | "time"
6 | )
7 |
8 | type Rate struct {
9 | bucketSize int64
10 | bucketSurplusSize int64
11 | bucketAddSize int64
12 | stopChan chan bool
13 | NowRate int64
14 | }
15 |
16 | func NewRate(addSize int64) *Rate {
17 | return &Rate{
18 | bucketSize: addSize * 2,
19 | bucketSurplusSize: 0,
20 | bucketAddSize: addSize,
21 | stopChan: make(chan bool),
22 | }
23 | }
24 |
25 | func (s *Rate) Start() {
26 | go s.session()
27 | }
28 |
29 | func (s *Rate) add(size int64) {
30 | if res := s.bucketSize - s.bucketSurplusSize; res < s.bucketAddSize {
31 | atomic.AddInt64(&s.bucketSurplusSize, res)
32 | return
33 | }
34 | atomic.AddInt64(&s.bucketSurplusSize, size)
35 | }
36 |
37 | //回桶
38 | func (s *Rate) ReturnBucket(size int64) {
39 | s.add(size)
40 | }
41 |
42 | //停止
43 | func (s *Rate) Stop() {
44 | s.stopChan <- true
45 | }
46 |
47 | func (s *Rate) Get(size int64) {
48 | if s.bucketSurplusSize >= size {
49 | atomic.AddInt64(&s.bucketSurplusSize, -size)
50 | return
51 | }
52 | ticker := time.NewTicker(time.Millisecond * 100)
53 | for {
54 | select {
55 | case <-ticker.C:
56 | if s.bucketSurplusSize >= size {
57 | atomic.AddInt64(&s.bucketSurplusSize, -size)
58 | ticker.Stop()
59 | return
60 | }
61 | }
62 | }
63 | }
64 |
65 | func (s *Rate) session() {
66 | ticker := time.NewTicker(time.Second * 1)
67 | for {
68 | select {
69 | case <-ticker.C:
70 | if rs := s.bucketAddSize - s.bucketSurplusSize; rs > 0 {
71 | s.NowRate = rs
72 | } else {
73 | s.NowRate = s.bucketSize - s.bucketSurplusSize
74 | }
75 | s.add(s.bucketAddSize)
76 | case <-s.stopChan:
77 | ticker.Stop()
78 | return
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/lib/sheap/heap.go:
--------------------------------------------------------------------------------
1 | package sheap
2 |
3 | type IntHeap []int64
4 |
5 | func (h IntHeap) Len() int { return len(h) }
6 | func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
7 | func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
8 |
9 | func (h *IntHeap) Push(x interface{}) {
10 | // Push and Pop use pointer receivers because they modify the slice's length,
11 | // not just its contents.
12 | *h = append(*h, x.(int64))
13 | }
14 |
15 | func (h *IntHeap) Pop() interface{} {
16 | old := *h
17 | n := len(old)
18 | x := old[n-1]
19 | *h = old[0 : n-1]
20 | return x
21 | }
22 |
--------------------------------------------------------------------------------
/lib/version/version.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | const VERSION = "0.26.10"
4 |
5 | // Compulsory minimum version, Minimum downward compatibility to this version
6 | func GetVersion() string {
7 | return "0.26.0"
8 | }
9 |
--------------------------------------------------------------------------------
/server/connection/connection.go:
--------------------------------------------------------------------------------
1 | package connection
2 |
3 | import (
4 | "net"
5 | "os"
6 | "strconv"
7 |
8 | "ehang.io/nps/lib/pmux"
9 | "github.com/astaxie/beego"
10 | "github.com/astaxie/beego/logs"
11 | )
12 |
13 | var pMux *pmux.PortMux
14 | var bridgePort string
15 | var httpsPort string
16 | var httpPort string
17 | var webPort string
18 |
19 | func InitConnectionService() {
20 | bridgePort = beego.AppConfig.String("bridge_port")
21 | httpsPort = beego.AppConfig.String("https_proxy_port")
22 | httpPort = beego.AppConfig.String("http_proxy_port")
23 | webPort = beego.AppConfig.String("web_port")
24 |
25 | if httpPort == bridgePort || httpsPort == bridgePort || webPort == bridgePort {
26 | port, err := strconv.Atoi(bridgePort)
27 | if err != nil {
28 | logs.Error(err)
29 | os.Exit(0)
30 | }
31 | pMux = pmux.NewPortMux(port, beego.AppConfig.String("web_host"))
32 | }
33 | }
34 |
35 | func GetBridgeListener(tp string) (net.Listener, error) {
36 | logs.Info("server start, the bridge type is %s, the bridge port is %s", tp, bridgePort)
37 | var p int
38 | var err error
39 | if p, err = strconv.Atoi(bridgePort); err != nil {
40 | return nil, err
41 | }
42 | if pMux != nil {
43 | return pMux.GetClientListener(), nil
44 | }
45 | return net.ListenTCP("tcp", &net.TCPAddr{net.ParseIP(beego.AppConfig.String("bridge_ip")), p, ""})
46 | }
47 |
48 | func GetHttpListener() (net.Listener, error) {
49 | if pMux != nil && httpPort == bridgePort {
50 | logs.Info("start http listener, port is", bridgePort)
51 | return pMux.GetHttpListener(), nil
52 | }
53 | logs.Info("start http listener, port is", httpPort)
54 | return getTcpListener(beego.AppConfig.String("http_proxy_ip"), httpPort)
55 | }
56 |
57 | func GetHttpsListener() (net.Listener, error) {
58 | if pMux != nil && httpsPort == bridgePort {
59 | logs.Info("start https listener, port is", bridgePort)
60 | return pMux.GetHttpsListener(), nil
61 | }
62 | logs.Info("start https listener, port is", httpsPort)
63 | return getTcpListener(beego.AppConfig.String("http_proxy_ip"), httpsPort)
64 | }
65 |
66 | func GetWebManagerListener() (net.Listener, error) {
67 | if pMux != nil && webPort == bridgePort {
68 | logs.Info("Web management start, access port is", bridgePort)
69 | return pMux.GetManagerListener(), nil
70 | }
71 | logs.Info("web management start, access port is", webPort)
72 | return getTcpListener(beego.AppConfig.String("web_ip"), webPort)
73 | }
74 |
75 | func getTcpListener(ip, p string) (net.Listener, error) {
76 | port, err := strconv.Atoi(p)
77 | if err != nil {
78 | logs.Error(err)
79 | os.Exit(0)
80 | }
81 | if ip == "" {
82 | ip = "0.0.0.0"
83 | }
84 | return net.ListenTCP("tcp", &net.TCPAddr{net.ParseIP(ip), port, ""})
85 | }
86 |
--------------------------------------------------------------------------------
/server/proxy/base.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "errors"
5 | "net"
6 | "net/http"
7 | "sync"
8 |
9 | "ehang.io/nps/bridge"
10 | "ehang.io/nps/lib/common"
11 | "ehang.io/nps/lib/conn"
12 | "ehang.io/nps/lib/file"
13 | "github.com/astaxie/beego/logs"
14 | )
15 |
16 | type Service interface {
17 | Start() error
18 | Close() error
19 | }
20 |
21 | type NetBridge interface {
22 | SendLinkInfo(clientId int, link *conn.Link, t *file.Tunnel) (target net.Conn, err error)
23 | }
24 |
25 | //BaseServer struct
26 | type BaseServer struct {
27 | id int
28 | bridge NetBridge
29 | task *file.Tunnel
30 | errorContent []byte
31 | sync.Mutex
32 | }
33 |
34 | func NewBaseServer(bridge *bridge.Bridge, task *file.Tunnel) *BaseServer {
35 | return &BaseServer{
36 | bridge: bridge,
37 | task: task,
38 | errorContent: nil,
39 | Mutex: sync.Mutex{},
40 | }
41 | }
42 |
43 | //add the flow
44 | func (s *BaseServer) FlowAdd(in, out int64) {
45 | s.Lock()
46 | defer s.Unlock()
47 | s.task.Flow.ExportFlow += out
48 | s.task.Flow.InletFlow += in
49 | }
50 |
51 | //change the flow
52 | func (s *BaseServer) FlowAddHost(host *file.Host, in, out int64) {
53 | s.Lock()
54 | defer s.Unlock()
55 | host.Flow.ExportFlow += out
56 | host.Flow.InletFlow += in
57 | }
58 |
59 | //write fail bytes to the connection
60 | func (s *BaseServer) writeConnFail(c net.Conn) {
61 | c.Write([]byte(common.ConnectionFailBytes))
62 | c.Write(s.errorContent)
63 | }
64 |
65 | //auth check
66 | func (s *BaseServer) auth(r *http.Request, c *conn.Conn, u, p string) error {
67 | if u != "" && p != "" && !common.CheckAuth(r, u, p) {
68 | c.Write([]byte(common.UnauthorizedBytes))
69 | c.Close()
70 | return errors.New("401 Unauthorized")
71 | }
72 | return nil
73 | }
74 |
75 | //check flow limit of the client ,and decrease the allow num of client
76 | func (s *BaseServer) CheckFlowAndConnNum(client *file.Client) error {
77 | if client.Flow.FlowLimit > 0 && (client.Flow.FlowLimit<<20) < (client.Flow.ExportFlow+client.Flow.InletFlow) {
78 | return errors.New("Traffic exceeded")
79 | }
80 | if !client.GetConn() {
81 | return errors.New("Connections exceed the current client limit")
82 | }
83 | return nil
84 | }
85 |
86 | //create a new connection and start bytes copying
87 | func (s *BaseServer) DealClient(c *conn.Conn, client *file.Client, addr string, rb []byte, tp string, f func(), flow *file.Flow, localProxy bool) error {
88 | link := conn.NewLink(tp, addr, client.Cnf.Crypt, client.Cnf.Compress, c.Conn.RemoteAddr().String(), localProxy)
89 | if target, err := s.bridge.SendLinkInfo(client.Id, link, s.task); err != nil {
90 | logs.Warn("get connection from client id %d error %s", client.Id, err.Error())
91 | c.Close()
92 | return err
93 | } else {
94 | if f != nil {
95 | f()
96 | }
97 | conn.CopyWaitGroup(target, c.Conn, link.Crypt, link.Compress, client.Rate, flow, true, rb)
98 | }
99 | return nil
100 | }
101 |
--------------------------------------------------------------------------------
/server/proxy/https.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "net"
5 | "net/http"
6 | "net/url"
7 | "sync"
8 |
9 | "ehang.io/nps/lib/cache"
10 | "ehang.io/nps/lib/common"
11 | "ehang.io/nps/lib/conn"
12 | "ehang.io/nps/lib/crypt"
13 | "ehang.io/nps/lib/file"
14 | "github.com/astaxie/beego"
15 | "github.com/astaxie/beego/logs"
16 | "github.com/pkg/errors"
17 | )
18 |
19 | type HttpsServer struct {
20 | httpServer
21 | listener net.Listener
22 | httpsListenerMap sync.Map
23 | }
24 |
25 | func NewHttpsServer(l net.Listener, bridge NetBridge, useCache bool, cacheLen int) *HttpsServer {
26 | https := &HttpsServer{listener: l}
27 | https.bridge = bridge
28 | https.useCache = useCache
29 | if useCache {
30 | https.cache = cache.New(cacheLen)
31 | }
32 | return https
33 | }
34 |
35 | //start https server
36 | func (https *HttpsServer) Start() error {
37 | if b, err := beego.AppConfig.Bool("https_just_proxy"); err == nil && b {
38 | conn.Accept(https.listener, func(c net.Conn) {
39 | https.handleHttps(c)
40 | })
41 | } else {
42 | //start the default listener
43 | certFile := beego.AppConfig.String("https_default_cert_file")
44 | keyFile := beego.AppConfig.String("https_default_key_file")
45 | if common.FileExists(certFile) && common.FileExists(keyFile) {
46 | l := NewHttpsListener(https.listener)
47 | https.NewHttps(l, certFile, keyFile)
48 | https.httpsListenerMap.Store("default", l)
49 | }
50 | conn.Accept(https.listener, func(c net.Conn) {
51 | serverName, rb := GetServerNameFromClientHello(c)
52 | //if the clientHello does not contains sni ,use the default ssl certificate
53 | if serverName == "" {
54 | serverName = "default"
55 | }
56 | var l *HttpsListener
57 | if v, ok := https.httpsListenerMap.Load(serverName); ok {
58 | l = v.(*HttpsListener)
59 | } else {
60 | r := buildHttpsRequest(serverName)
61 | if host, err := file.GetDb().GetInfoByHost(serverName, r); err != nil {
62 | c.Close()
63 | logs.Notice("the url %s can't be parsed!,remote addr %s", serverName, c.RemoteAddr().String())
64 | return
65 | } else {
66 | if !common.FileExists(host.CertFilePath) || !common.FileExists(host.KeyFilePath) {
67 | //if the host cert file or key file is not set ,use the default file
68 | if v, ok := https.httpsListenerMap.Load("default"); ok {
69 | l = v.(*HttpsListener)
70 | } else {
71 | c.Close()
72 | logs.Error("the key %s cert %s file is not exist", host.KeyFilePath, host.CertFilePath)
73 | return
74 | }
75 | } else {
76 | l = NewHttpsListener(https.listener)
77 | https.NewHttps(l, host.CertFilePath, host.KeyFilePath)
78 | https.httpsListenerMap.Store(serverName, l)
79 | }
80 | }
81 | }
82 | acceptConn := conn.NewConn(c)
83 | acceptConn.Rb = rb
84 | l.acceptConn <- acceptConn
85 | })
86 | }
87 | return nil
88 | }
89 |
90 | // close
91 | func (https *HttpsServer) Close() error {
92 | return https.listener.Close()
93 | }
94 |
95 | // new https server by cert and key file
96 | func (https *HttpsServer) NewHttps(l net.Listener, certFile string, keyFile string) {
97 | go func() {
98 | logs.Error(https.NewServer(0, "https").ServeTLS(l, certFile, keyFile))
99 | }()
100 | }
101 |
102 | //handle the https which is just proxy to other client
103 | func (https *HttpsServer) handleHttps(c net.Conn) {
104 | hostName, rb := GetServerNameFromClientHello(c)
105 | var targetAddr string
106 | r := buildHttpsRequest(hostName)
107 | var host *file.Host
108 | var err error
109 | if host, err = file.GetDb().GetInfoByHost(hostName, r); err != nil {
110 | c.Close()
111 | logs.Notice("the url %s can't be parsed!", hostName)
112 | return
113 | }
114 | if err := https.CheckFlowAndConnNum(host.Client); err != nil {
115 | logs.Warn("client id %d, host id %d, error %s, when https connection", host.Client.Id, host.Id, err.Error())
116 | c.Close()
117 | return
118 | }
119 | defer host.Client.AddConn()
120 | if err = https.auth(r, conn.NewConn(c), host.Client.Cnf.U, host.Client.Cnf.P); err != nil {
121 | logs.Warn("auth error", err, r.RemoteAddr)
122 | return
123 | }
124 | if targetAddr, err = host.Target.GetRandomTarget(); err != nil {
125 | logs.Warn(err.Error())
126 | }
127 | logs.Trace("new https connection,clientId %d,host %s,remote address %s", host.Client.Id, r.Host, c.RemoteAddr().String())
128 | https.DealClient(conn.NewConn(c), host.Client, targetAddr, rb, common.CONN_TCP, nil, host.Flow, host.Target.LocalProxy)
129 | }
130 |
131 | type HttpsListener struct {
132 | acceptConn chan *conn.Conn
133 | parentListener net.Listener
134 | }
135 |
136 | // https listener
137 | func NewHttpsListener(l net.Listener) *HttpsListener {
138 | return &HttpsListener{parentListener: l, acceptConn: make(chan *conn.Conn)}
139 | }
140 |
141 | // accept
142 | func (httpsListener *HttpsListener) Accept() (net.Conn, error) {
143 | httpsConn := <-httpsListener.acceptConn
144 | if httpsConn == nil {
145 | return nil, errors.New("get connection error")
146 | }
147 | return httpsConn, nil
148 | }
149 |
150 | // close
151 | func (httpsListener *HttpsListener) Close() error {
152 | return nil
153 | }
154 |
155 | // addr
156 | func (httpsListener *HttpsListener) Addr() net.Addr {
157 | return httpsListener.parentListener.Addr()
158 | }
159 |
160 | // get server name from connection by read client hello bytes
161 | func GetServerNameFromClientHello(c net.Conn) (string, []byte) {
162 | buf := make([]byte, 4096)
163 | data := make([]byte, 4096)
164 | n, err := c.Read(buf)
165 | if err != nil {
166 | return "", nil
167 | }
168 | if n < 42 {
169 | return "", nil
170 | }
171 | copy(data, buf[:n])
172 | clientHello := new(crypt.ClientHelloMsg)
173 | clientHello.Unmarshal(data[5:n])
174 | return clientHello.GetServerName(), buf[:n]
175 | }
176 |
177 | // build https request
178 | func buildHttpsRequest(hostName string) *http.Request {
179 | r := new(http.Request)
180 | r.RequestURI = "/"
181 | r.URL = new(url.URL)
182 | r.URL.Scheme = "https"
183 | r.Host = hostName
184 | return r
185 | }
186 |
--------------------------------------------------------------------------------
/server/proxy/p2p.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "net"
5 | "strings"
6 | "time"
7 |
8 | "ehang.io/nps/lib/common"
9 | "github.com/astaxie/beego/logs"
10 | )
11 |
12 | type P2PServer struct {
13 | BaseServer
14 | p2pPort int
15 | p2p map[string]*p2p
16 | listener *net.UDPConn
17 | }
18 |
19 | type p2p struct {
20 | visitorAddr *net.UDPAddr
21 | providerAddr *net.UDPAddr
22 | }
23 |
24 | func NewP2PServer(p2pPort int) *P2PServer {
25 | return &P2PServer{
26 | p2pPort: p2pPort,
27 | p2p: make(map[string]*p2p),
28 | }
29 | }
30 |
31 | func (s *P2PServer) Start() error {
32 | logs.Info("start p2p server port", s.p2pPort)
33 | var err error
34 | s.listener, err = net.ListenUDP("udp", &net.UDPAddr{net.ParseIP("0.0.0.0"), s.p2pPort, ""})
35 | if err != nil {
36 | return err
37 | }
38 | for {
39 | buf := common.BufPoolUdp.Get().([]byte)
40 | n, addr, err := s.listener.ReadFromUDP(buf)
41 | if err != nil {
42 | if strings.Contains(err.Error(), "use of closed network connection") {
43 | break
44 | }
45 | continue
46 | }
47 | go s.handleP2P(addr, string(buf[:n]))
48 | }
49 | return nil
50 | }
51 |
52 | func (s *P2PServer) handleP2P(addr *net.UDPAddr, str string) {
53 | var (
54 | v *p2p
55 | ok bool
56 | )
57 | arr := strings.Split(str, common.CONN_DATA_SEQ)
58 | if len(arr) < 2 {
59 | return
60 | }
61 | if v, ok = s.p2p[arr[0]]; !ok {
62 | v = new(p2p)
63 | s.p2p[arr[0]] = v
64 | }
65 | logs.Trace("new p2p connection ,role %s , password %s ,local address %s", arr[1], arr[0], addr.String())
66 | if arr[1] == common.WORK_P2P_VISITOR {
67 | v.visitorAddr = addr
68 | for i := 20; i > 0; i-- {
69 | if v.providerAddr != nil {
70 | s.listener.WriteTo([]byte(v.providerAddr.String()), v.visitorAddr)
71 | s.listener.WriteTo([]byte(v.visitorAddr.String()), v.providerAddr)
72 | break
73 | }
74 | time.Sleep(time.Second)
75 | }
76 | delete(s.p2p, arr[0])
77 | } else {
78 | v.providerAddr = addr
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/server/proxy/tcp.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "errors"
5 | "net"
6 | "net/http"
7 | "path/filepath"
8 | "strconv"
9 |
10 | "ehang.io/nps/bridge"
11 | "ehang.io/nps/lib/common"
12 | "ehang.io/nps/lib/conn"
13 | "ehang.io/nps/lib/file"
14 | "ehang.io/nps/server/connection"
15 | "github.com/astaxie/beego"
16 | "github.com/astaxie/beego/logs"
17 | )
18 |
19 | type TunnelModeServer struct {
20 | BaseServer
21 | process process
22 | listener net.Listener
23 | }
24 |
25 | //tcp|http|host
26 | func NewTunnelModeServer(process process, bridge NetBridge, task *file.Tunnel) *TunnelModeServer {
27 | s := new(TunnelModeServer)
28 | s.bridge = bridge
29 | s.process = process
30 | s.task = task
31 | return s
32 | }
33 |
34 | //开始
35 | func (s *TunnelModeServer) Start() error {
36 | return conn.NewTcpListenerAndProcess(s.task.ServerIp+":"+strconv.Itoa(s.task.Port), func(c net.Conn) {
37 | if err := s.CheckFlowAndConnNum(s.task.Client); err != nil {
38 | logs.Warn("client id %d, task id %d,error %s, when tcp connection", s.task.Client.Id, s.task.Id, err.Error())
39 | c.Close()
40 | return
41 | }
42 | logs.Trace("new tcp connection,local port %d,client %d,remote address %s", s.task.Port, s.task.Client.Id, c.RemoteAddr())
43 | s.process(conn.NewConn(c), s)
44 | s.task.Client.AddConn()
45 | }, &s.listener)
46 | }
47 |
48 | //close
49 | func (s *TunnelModeServer) Close() error {
50 | return s.listener.Close()
51 | }
52 |
53 | //web管理方式
54 | type WebServer struct {
55 | BaseServer
56 | }
57 |
58 | //开始
59 | func (s *WebServer) Start() error {
60 | p, _ := beego.AppConfig.Int("web_port")
61 | if p == 0 {
62 | stop := make(chan struct{})
63 | <-stop
64 | }
65 | beego.BConfig.WebConfig.Session.SessionOn = true
66 | beego.SetStaticPath(beego.AppConfig.String("web_base_url")+"/static", filepath.Join(common.GetRunPath(), "web", "static"))
67 | beego.SetViewsPath(filepath.Join(common.GetRunPath(), "web", "views"))
68 | err := errors.New("Web management startup failure ")
69 | var l net.Listener
70 | if l, err = connection.GetWebManagerListener(); err == nil {
71 | beego.InitBeforeHTTPRun()
72 | if beego.AppConfig.String("web_open_ssl") == "true" {
73 | keyPath := beego.AppConfig.String("web_key_file")
74 | certPath := beego.AppConfig.String("web_cert_file")
75 | err = http.ServeTLS(l, beego.BeeApp.Handlers, certPath, keyPath)
76 | } else {
77 | err = http.Serve(l, beego.BeeApp.Handlers)
78 | }
79 | } else {
80 | logs.Error(err)
81 | }
82 | return err
83 | }
84 |
85 | func (s *WebServer) Close() error {
86 | return nil
87 | }
88 |
89 | //new
90 | func NewWebServer(bridge *bridge.Bridge) *WebServer {
91 | s := new(WebServer)
92 | s.bridge = bridge
93 | return s
94 | }
95 |
96 | type process func(c *conn.Conn, s *TunnelModeServer) error
97 |
98 | //tcp proxy
99 | func ProcessTunnel(c *conn.Conn, s *TunnelModeServer) error {
100 | targetAddr, err := s.task.Target.GetRandomTarget()
101 | if err != nil {
102 | c.Close()
103 | logs.Warn("tcp port %d ,client id %d,task id %d connect error %s", s.task.Port, s.task.Client.Id, s.task.Id, err.Error())
104 | return err
105 | }
106 | return s.DealClient(c, s.task.Client, targetAddr, nil, common.CONN_TCP, nil, s.task.Flow, s.task.Target.LocalProxy)
107 | }
108 |
109 | //http proxy
110 | func ProcessHttp(c *conn.Conn, s *TunnelModeServer) error {
111 | _, addr, rb, err, r := c.GetHost()
112 | if err != nil {
113 | c.Close()
114 | logs.Info(err)
115 | return err
116 | }
117 | if r.Method == "CONNECT" {
118 | c.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
119 | rb = nil
120 | }
121 | if err := s.auth(r, c, s.task.Client.Cnf.U, s.task.Client.Cnf.P); err != nil {
122 | return err
123 | }
124 | return s.DealClient(c, s.task.Client, addr, rb, common.CONN_TCP, nil, s.task.Flow, s.task.Target.LocalProxy)
125 | }
126 |
--------------------------------------------------------------------------------
/server/proxy/transport.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package proxy
4 |
5 | import (
6 | "net"
7 | "strconv"
8 | "syscall"
9 |
10 | "ehang.io/nps/lib/common"
11 | "ehang.io/nps/lib/conn"
12 | )
13 |
14 | func HandleTrans(c *conn.Conn, s *TunnelModeServer) error {
15 | if addr, err := getAddress(c.Conn); err != nil {
16 | return err
17 | } else {
18 | return s.DealClient(c, s.task.Client, addr, nil, common.CONN_TCP, nil, s.task.Flow, s.task.Target.LocalProxy)
19 | }
20 | }
21 |
22 | const SO_ORIGINAL_DST = 80
23 |
24 | func getAddress(conn net.Conn) (string, error) {
25 | sysrawconn, f := conn.(syscall.Conn)
26 | if !f {
27 | return "", nil
28 | }
29 | rawConn, err := sysrawconn.SyscallConn()
30 | if err != nil {
31 | return "", nil
32 | }
33 | var ip string
34 | var port uint16
35 | err = rawConn.Control(func(fd uintptr) {
36 | addr, err := syscall.GetsockoptIPv6Mreq(int(fd), syscall.IPPROTO_IP, SO_ORIGINAL_DST)
37 | if err != nil {
38 | return
39 | }
40 | ip = net.IP(addr.Multiaddr[4:8]).String()
41 | port = uint16(addr.Multiaddr[2])<<8 + uint16(addr.Multiaddr[3])
42 | })
43 | return ip + ":" + strconv.Itoa(int(port)), nil
44 | }
45 |
--------------------------------------------------------------------------------
/server/proxy/transport_windows.go:
--------------------------------------------------------------------------------
1 | // +build windows
2 |
3 | package proxy
4 |
5 | import (
6 | "ehang.io/nps/lib/conn"
7 | )
8 |
9 | func HandleTrans(c *conn.Conn, s *TunnelModeServer) error {
10 | return nil
11 | }
12 |
--------------------------------------------------------------------------------
/server/proxy/udp.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "io"
5 | "net"
6 | "strings"
7 | "sync"
8 | "time"
9 |
10 | "ehang.io/nps/bridge"
11 | "ehang.io/nps/lib/common"
12 | "ehang.io/nps/lib/conn"
13 | "ehang.io/nps/lib/file"
14 | "github.com/astaxie/beego/logs"
15 | )
16 |
17 | type UdpModeServer struct {
18 | BaseServer
19 | addrMap sync.Map
20 | listener *net.UDPConn
21 | }
22 |
23 | func NewUdpModeServer(bridge *bridge.Bridge, task *file.Tunnel) *UdpModeServer {
24 | s := new(UdpModeServer)
25 | s.bridge = bridge
26 | s.task = task
27 | return s
28 | }
29 |
30 | //开始
31 | func (s *UdpModeServer) Start() error {
32 | var err error
33 | if s.task.ServerIp == "" {
34 | s.task.ServerIp = "0.0.0.0"
35 | }
36 | s.listener, err = net.ListenUDP("udp", &net.UDPAddr{net.ParseIP(s.task.ServerIp), s.task.Port, ""})
37 | if err != nil {
38 | return err
39 | }
40 | for {
41 | buf := common.BufPoolUdp.Get().([]byte)
42 | n, addr, err := s.listener.ReadFromUDP(buf)
43 | if err != nil {
44 | if strings.Contains(err.Error(), "use of closed network connection") {
45 | break
46 | }
47 | continue
48 | }
49 | logs.Trace("New udp connection,client %d,remote address %s", s.task.Client.Id, addr)
50 | go s.process(addr, buf[:n])
51 | }
52 | return nil
53 | }
54 |
55 | func (s *UdpModeServer) process(addr *net.UDPAddr, data []byte) {
56 | if v, ok := s.addrMap.Load(addr.String()); ok {
57 | clientConn, ok := v.(io.ReadWriteCloser)
58 | if ok {
59 | clientConn.Write(data)
60 | s.task.Flow.Add(int64(len(data)), 0)
61 | }
62 | } else {
63 | if err := s.CheckFlowAndConnNum(s.task.Client); err != nil {
64 | logs.Warn("client id %d, task id %d,error %s, when udp connection", s.task.Client.Id, s.task.Id, err.Error())
65 | return
66 | }
67 | defer s.task.Client.AddConn()
68 | link := conn.NewLink(common.CONN_UDP, s.task.Target.TargetStr, s.task.Client.Cnf.Crypt, s.task.Client.Cnf.Compress, addr.String(), s.task.Target.LocalProxy)
69 | if clientConn, err := s.bridge.SendLinkInfo(s.task.Client.Id, link, s.task); err != nil {
70 | return
71 | } else {
72 | target := conn.GetConn(clientConn, s.task.Client.Cnf.Crypt, s.task.Client.Cnf.Compress, nil, true)
73 | s.addrMap.Store(addr.String(), target)
74 | defer target.Close()
75 |
76 | target.Write(data)
77 |
78 | buf := common.BufPoolUdp.Get().([]byte)
79 | defer common.BufPoolUdp.Put(buf)
80 |
81 | s.task.Flow.Add(int64(len(data)), 0)
82 | for {
83 | clientConn.SetReadDeadline(time.Now().Add(time.Minute * 10))
84 | if n, err := target.Read(buf); err != nil {
85 | s.addrMap.Delete(addr.String())
86 | logs.Warn(err)
87 | return
88 | } else {
89 | s.listener.WriteTo(buf[:n], addr)
90 | s.task.Flow.Add(0, int64(n))
91 | }
92 | }
93 | }
94 | }
95 | }
96 |
97 | func (s *UdpModeServer) Close() error {
98 | return s.listener.Close()
99 | }
100 |
--------------------------------------------------------------------------------
/server/test/test.go:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | import (
4 | "log"
5 | "path/filepath"
6 | "strconv"
7 |
8 | "ehang.io/nps/lib/common"
9 | "ehang.io/nps/lib/file"
10 | "github.com/astaxie/beego"
11 | )
12 |
13 | func TestServerConfig() {
14 | var postTcpArr []int
15 | var postUdpArr []int
16 | file.GetDb().JsonDb.Tasks.Range(func(key, value interface{}) bool {
17 | v := value.(*file.Tunnel)
18 | if v.Mode == "udp" {
19 | isInArr(&postUdpArr, v.Port, v.Remark, "udp")
20 | } else if v.Port != 0 {
21 |
22 | isInArr(&postTcpArr, v.Port, v.Remark, "tcp")
23 | }
24 | return true
25 | })
26 | p, err := beego.AppConfig.Int("web_port")
27 | if err != nil {
28 | log.Fatalln("Getting web management port error :", err)
29 | } else {
30 | isInArr(&postTcpArr, p, "Web Management port", "tcp")
31 | }
32 |
33 | if p := beego.AppConfig.String("bridge_port"); p != "" {
34 | if port, err := strconv.Atoi(p); err != nil {
35 | log.Fatalln("get Server and client communication portserror:", err)
36 | } else if beego.AppConfig.String("bridge_type") == "kcp" {
37 | isInArr(&postUdpArr, port, "Server and client communication ports", "udp")
38 | } else {
39 | isInArr(&postTcpArr, port, "Server and client communication ports", "tcp")
40 | }
41 | }
42 |
43 | if p := beego.AppConfig.String("httpProxyPort"); p != "" {
44 | if port, err := strconv.Atoi(p); err != nil {
45 | log.Fatalln("get http port error:", err)
46 | } else {
47 | isInArr(&postTcpArr, port, "https port", "tcp")
48 | }
49 | }
50 | if p := beego.AppConfig.String("https_proxy_port"); p != "" {
51 | if b, err := beego.AppConfig.Bool("https_just_proxy"); !(err == nil && b) {
52 | if port, err := strconv.Atoi(p); err != nil {
53 | log.Fatalln("get https port error", err)
54 | } else {
55 | if beego.AppConfig.String("pemPath") != "" && !common.FileExists(filepath.Join(common.GetRunPath(), beego.AppConfig.String("pemPath"))) {
56 | log.Fatalf("ssl certFile %s is not exist", beego.AppConfig.String("pemPath"))
57 | }
58 | if beego.AppConfig.String("keyPath") != "" && !common.FileExists(filepath.Join(common.GetRunPath(), beego.AppConfig.String("keyPath"))) {
59 | log.Fatalf("ssl keyFile %s is not exist", beego.AppConfig.String("pemPath"))
60 | }
61 | isInArr(&postTcpArr, port, "http port", "tcp")
62 | }
63 | }
64 | }
65 | }
66 |
67 | func isInArr(arr *[]int, val int, remark string, tp string) {
68 | for _, v := range *arr {
69 | if v == val {
70 | log.Fatalf("the port %d is reused,remark: %s", val, remark)
71 | }
72 | }
73 | if tp == "tcp" {
74 | if !common.TestTcpPort(val) {
75 | log.Fatalf("open the %d port error ,remark: %s", val, remark)
76 | }
77 | } else {
78 | if !common.TestUdpPort(val) {
79 | log.Fatalf("open the %d port error ,remark: %s", val, remark)
80 | }
81 | }
82 | *arr = append(*arr, val)
83 | return
84 | }
85 |
--------------------------------------------------------------------------------
/server/tool/utils.go:
--------------------------------------------------------------------------------
1 | package tool
2 |
3 | import (
4 | "math"
5 | "strconv"
6 | "time"
7 |
8 | "ehang.io/nps/lib/common"
9 | "github.com/astaxie/beego"
10 | "github.com/shirou/gopsutil/v3/cpu"
11 | "github.com/shirou/gopsutil/v3/load"
12 | "github.com/shirou/gopsutil/v3/mem"
13 | "github.com/shirou/gopsutil/v3/net"
14 | )
15 |
16 | var (
17 | ports []int
18 | ServerStatus []map[string]interface{}
19 | )
20 |
21 | func StartSystemInfo() {
22 | if b, err := beego.AppConfig.Bool("system_info_display"); err == nil && b {
23 | ServerStatus = make([]map[string]interface{}, 0, 1500)
24 | go getSeverStatus()
25 | }
26 | }
27 |
28 | func InitAllowPort() {
29 | p := beego.AppConfig.String("allow_ports")
30 | ports = common.GetPorts(p)
31 | }
32 |
33 | func TestServerPort(p int, m string) (b bool) {
34 | if m == "p2p" || m == "secret" {
35 | return true
36 | }
37 | if p > 65535 || p < 0 {
38 | return false
39 | }
40 | if len(ports) != 0 {
41 | if !common.InIntArr(ports, p) {
42 | return false
43 | }
44 | }
45 | if m == "udp" {
46 | b = common.TestUdpPort(p)
47 | } else {
48 | b = common.TestTcpPort(p)
49 | }
50 | return
51 | }
52 |
53 | func getSeverStatus() {
54 | for {
55 | if len(ServerStatus) < 10 {
56 | time.Sleep(time.Second)
57 | } else {
58 | time.Sleep(time.Minute)
59 | }
60 | cpuPercet, _ := cpu.Percent(0, true)
61 | var cpuAll float64
62 | for _, v := range cpuPercet {
63 | cpuAll += v
64 | }
65 | m := make(map[string]interface{})
66 | loads, _ := load.Avg()
67 | m["load1"] = loads.Load1
68 | m["load5"] = loads.Load5
69 | m["load15"] = loads.Load15
70 | m["cpu"] = math.Round(cpuAll / float64(len(cpuPercet)))
71 | swap, _ := mem.SwapMemory()
72 | m["swap_mem"] = math.Round(swap.UsedPercent)
73 | vir, _ := mem.VirtualMemory()
74 | m["virtual_mem"] = math.Round(vir.UsedPercent)
75 | conn, _ := net.ProtoCounters(nil)
76 | io1, _ := net.IOCounters(false)
77 | time.Sleep(time.Millisecond * 500)
78 | io2, _ := net.IOCounters(false)
79 | if len(io2) > 0 && len(io1) > 0 {
80 | m["io_send"] = (io2[0].BytesSent - io1[0].BytesSent) * 2
81 | m["io_recv"] = (io2[0].BytesRecv - io1[0].BytesRecv) * 2
82 | }
83 | t := time.Now()
84 | m["time"] = strconv.Itoa(t.Hour()) + ":" + strconv.Itoa(t.Minute()) + ":" + strconv.Itoa(t.Second())
85 |
86 | for _, v := range conn {
87 | m[v.Protocol] = v.Stats["CurrEstab"]
88 | }
89 | if len(ServerStatus) >= 1440 {
90 | ServerStatus = ServerStatus[1:]
91 | }
92 | ServerStatus = append(ServerStatus, m)
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/web/controllers/auth.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "encoding/hex"
5 | "time"
6 |
7 | "ehang.io/nps/lib/crypt"
8 | "github.com/astaxie/beego"
9 | )
10 |
11 | type AuthController struct {
12 | beego.Controller
13 | }
14 |
15 | func (s *AuthController) GetAuthKey() {
16 | m := make(map[string]interface{})
17 | defer func() {
18 | s.Data["json"] = m
19 | s.ServeJSON()
20 | }()
21 | if cryptKey := beego.AppConfig.String("auth_crypt_key"); len(cryptKey) != 16 {
22 | m["status"] = 0
23 | return
24 | } else {
25 | b, err := crypt.AesEncrypt([]byte(beego.AppConfig.String("auth_key")), []byte(cryptKey))
26 | if err != nil {
27 | m["status"] = 0
28 | return
29 | }
30 | m["status"] = 1
31 | m["crypt_auth_key"] = hex.EncodeToString(b)
32 | m["crypt_type"] = "aes cbc"
33 | return
34 | }
35 | }
36 |
37 | func (s *AuthController) GetTime() {
38 | m := make(map[string]interface{})
39 | m["time"] = time.Now().Unix()
40 | s.Data["json"] = m
41 | s.ServeJSON()
42 | }
43 |
--------------------------------------------------------------------------------
/web/controllers/base.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "html"
5 | "math"
6 | "strconv"
7 | "strings"
8 | "time"
9 |
10 | "ehang.io/nps/lib/common"
11 | "ehang.io/nps/lib/crypt"
12 | "ehang.io/nps/lib/file"
13 | "ehang.io/nps/server"
14 | "github.com/astaxie/beego"
15 | )
16 |
17 | type BaseController struct {
18 | beego.Controller
19 | controllerName string
20 | actionName string
21 | }
22 |
23 | //初始化参数
24 | func (s *BaseController) Prepare() {
25 | s.Data["web_base_url"] = beego.AppConfig.String("web_base_url")
26 | controllerName, actionName := s.GetControllerAndAction()
27 | s.controllerName = strings.ToLower(controllerName[0 : len(controllerName)-10])
28 | s.actionName = strings.ToLower(actionName)
29 | // web api verify
30 | // param 1 is md5(authKey+Current timestamp)
31 | // param 2 is timestamp (It's limited to 20 seconds.)
32 | md5Key := s.getEscapeString("auth_key")
33 | timestamp := s.GetIntNoErr("timestamp")
34 | configKey := beego.AppConfig.String("auth_key")
35 | timeNowUnix := time.Now().Unix()
36 | if !(md5Key != "" && (math.Abs(float64(timeNowUnix-int64(timestamp))) <= 20) && (crypt.Md5(configKey+strconv.Itoa(timestamp)) == md5Key)) {
37 | if s.GetSession("auth") != true {
38 | s.Redirect(beego.AppConfig.String("web_base_url")+"/login/index", 302)
39 | }
40 | } else {
41 | s.SetSession("isAdmin", true)
42 | s.Data["isAdmin"] = true
43 | }
44 | if s.GetSession("isAdmin") != nil && !s.GetSession("isAdmin").(bool) {
45 | s.Ctx.Input.SetData("client_id", s.GetSession("clientId").(int))
46 | s.Ctx.Input.SetParam("client_id", strconv.Itoa(s.GetSession("clientId").(int)))
47 | s.Data["isAdmin"] = false
48 | s.Data["username"] = s.GetSession("username")
49 | s.CheckUserAuth()
50 | } else {
51 | s.Data["isAdmin"] = true
52 | }
53 | s.Data["https_just_proxy"], _ = beego.AppConfig.Bool("https_just_proxy")
54 | s.Data["allow_user_login"], _ = beego.AppConfig.Bool("allow_user_login")
55 | s.Data["allow_flow_limit"], _ = beego.AppConfig.Bool("allow_flow_limit")
56 | s.Data["allow_rate_limit"], _ = beego.AppConfig.Bool("allow_rate_limit")
57 | s.Data["allow_connection_num_limit"], _ = beego.AppConfig.Bool("allow_connection_num_limit")
58 | s.Data["allow_multi_ip"], _ = beego.AppConfig.Bool("allow_multi_ip")
59 | s.Data["system_info_display"], _ = beego.AppConfig.Bool("system_info_display")
60 | s.Data["allow_tunnel_num_limit"], _ = beego.AppConfig.Bool("allow_tunnel_num_limit")
61 | s.Data["allow_local_proxy"], _ = beego.AppConfig.Bool("allow_local_proxy")
62 | s.Data["allow_user_change_username"], _ = beego.AppConfig.Bool("allow_user_change_username")
63 | }
64 |
65 | //加载模板
66 | func (s *BaseController) display(tpl ...string) {
67 | s.Data["web_base_url"] = beego.AppConfig.String("web_base_url")
68 | var tplname string
69 | if s.Data["menu"] == nil {
70 | s.Data["menu"] = s.actionName
71 | }
72 | if len(tpl) > 0 {
73 | tplname = strings.Join([]string{tpl[0], "html"}, ".")
74 | } else {
75 | tplname = s.controllerName + "/" + s.actionName + ".html"
76 | }
77 | ip := s.Ctx.Request.Host
78 | s.Data["ip"] = common.GetIpByAddr(ip)
79 | s.Data["bridgeType"] = beego.AppConfig.String("bridge_type")
80 | if common.IsWindows() {
81 | s.Data["win"] = ".exe"
82 | }
83 | s.Data["p"] = server.Bridge.TunnelPort
84 | s.Data["proxyPort"] = beego.AppConfig.String("hostPort")
85 | s.Layout = "public/layout.html"
86 | s.TplName = tplname
87 | }
88 |
89 | //错误
90 | func (s *BaseController) error() {
91 | s.Data["web_base_url"] = beego.AppConfig.String("web_base_url")
92 | s.Layout = "public/layout.html"
93 | s.TplName = "public/error.html"
94 | }
95 |
96 | //getEscapeString
97 | func (s *BaseController) getEscapeString(key string) string {
98 | return html.EscapeString(s.GetString(key))
99 | }
100 |
101 | //去掉没有err返回值的int
102 | func (s *BaseController) GetIntNoErr(key string, def ...int) int {
103 | strv := s.Ctx.Input.Query(key)
104 | if len(strv) == 0 && len(def) > 0 {
105 | return def[0]
106 | }
107 | val, _ := strconv.Atoi(strv)
108 | return val
109 | }
110 |
111 | //获取去掉错误的bool值
112 | func (s *BaseController) GetBoolNoErr(key string, def ...bool) bool {
113 | strv := s.Ctx.Input.Query(key)
114 | if len(strv) == 0 && len(def) > 0 {
115 | return def[0]
116 | }
117 | val, _ := strconv.ParseBool(strv)
118 | return val
119 | }
120 |
121 | //ajax正确返回
122 | func (s *BaseController) AjaxOk(str string) {
123 | s.Data["json"] = ajax(str, 1)
124 | s.ServeJSON()
125 | s.StopRun()
126 | }
127 |
128 | //ajax错误返回
129 | func (s *BaseController) AjaxErr(str string) {
130 | s.Data["json"] = ajax(str, 0)
131 | s.ServeJSON()
132 | s.StopRun()
133 | }
134 |
135 | //组装ajax
136 | func ajax(str string, status int) map[string]interface{} {
137 | json := make(map[string]interface{})
138 | json["status"] = status
139 | json["msg"] = str
140 | return json
141 | }
142 |
143 | //ajax table返回
144 | func (s *BaseController) AjaxTable(list interface{}, cnt int, recordsTotal int, kwargs map[string]interface{}) {
145 | json := make(map[string]interface{})
146 | json["rows"] = list
147 | json["total"] = recordsTotal
148 | if kwargs != nil {
149 | for k, v := range kwargs {
150 | if v != nil {
151 | json[k] = v
152 | }
153 | }
154 | }
155 | s.Data["json"] = json
156 | s.ServeJSON()
157 | s.StopRun()
158 | }
159 |
160 | //ajax table参数
161 | func (s *BaseController) GetAjaxParams() (start, limit int) {
162 | return s.GetIntNoErr("offset"), s.GetIntNoErr("limit")
163 | }
164 |
165 | func (s *BaseController) SetInfo(name string) {
166 | s.Data["name"] = name
167 | }
168 |
169 | func (s *BaseController) SetType(name string) {
170 | s.Data["type"] = name
171 | }
172 |
173 | func (s *BaseController) CheckUserAuth() {
174 | if s.controllerName == "client" {
175 | if s.actionName == "add" {
176 | s.StopRun()
177 | return
178 | }
179 | if id := s.GetIntNoErr("id"); id != 0 {
180 | if id != s.GetSession("clientId").(int) {
181 | s.StopRun()
182 | return
183 | }
184 | }
185 | }
186 | if s.controllerName == "index" {
187 | if id := s.GetIntNoErr("id"); id != 0 {
188 | belong := false
189 | if strings.Contains(s.actionName, "h") {
190 | if v, ok := file.GetDb().JsonDb.Hosts.Load(id); ok {
191 | if v.(*file.Host).Client.Id == s.GetSession("clientId").(int) {
192 | belong = true
193 | }
194 | }
195 | } else {
196 | if v, ok := file.GetDb().JsonDb.Tasks.Load(id); ok {
197 | if v.(*file.Tunnel).Client.Id == s.GetSession("clientId").(int) {
198 | belong = true
199 | }
200 | }
201 | }
202 | if !belong {
203 | s.StopRun()
204 | }
205 | }
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/web/controllers/client.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "ehang.io/nps/lib/common"
5 | "ehang.io/nps/lib/file"
6 | "ehang.io/nps/lib/rate"
7 | "ehang.io/nps/server"
8 | "github.com/astaxie/beego"
9 | )
10 |
11 | type ClientController struct {
12 | BaseController
13 | }
14 |
15 | func (s *ClientController) List() {
16 | if s.Ctx.Request.Method == "GET" {
17 | s.Data["menu"] = "client"
18 | s.SetInfo("client")
19 | s.display("client/list")
20 | return
21 | }
22 | start, length := s.GetAjaxParams()
23 | clientIdSession := s.GetSession("clientId")
24 | var clientId int
25 | if clientIdSession == nil {
26 | clientId = 0
27 | } else {
28 | clientId = clientIdSession.(int)
29 | }
30 | list, cnt := server.GetClientList(start, length, s.getEscapeString("search"), s.getEscapeString("sort"), s.getEscapeString("order"), clientId)
31 | cmd := make(map[string]interface{})
32 | ip := s.Ctx.Request.Host
33 | cmd["ip"] = common.GetIpByAddr(ip)
34 | cmd["bridgeType"] = beego.AppConfig.String("bridge_type")
35 | cmd["bridgePort"] = server.Bridge.TunnelPort
36 | s.AjaxTable(list, cnt, cnt, cmd)
37 | }
38 |
39 | //添加客户端
40 | func (s *ClientController) Add() {
41 | if s.Ctx.Request.Method == "GET" {
42 | s.Data["menu"] = "client"
43 | s.SetInfo("add client")
44 | s.display()
45 | } else {
46 | t := &file.Client{
47 | VerifyKey: s.getEscapeString("vkey"),
48 | Id: int(file.GetDb().JsonDb.GetClientId()),
49 | Status: true,
50 | Remark: s.getEscapeString("remark"),
51 | Cnf: &file.Config{
52 | U: s.getEscapeString("u"),
53 | P: s.getEscapeString("p"),
54 | Compress: common.GetBoolByStr(s.getEscapeString("compress")),
55 | Crypt: s.GetBoolNoErr("crypt"),
56 | },
57 | ConfigConnAllow: s.GetBoolNoErr("config_conn_allow"),
58 | RateLimit: s.GetIntNoErr("rate_limit"),
59 | MaxConn: s.GetIntNoErr("max_conn"),
60 | WebUserName: s.getEscapeString("web_username"),
61 | WebPassword: s.getEscapeString("web_password"),
62 | MaxTunnelNum: s.GetIntNoErr("max_tunnel"),
63 | Flow: &file.Flow{
64 | ExportFlow: 0,
65 | InletFlow: 0,
66 | FlowLimit: int64(s.GetIntNoErr("flow_limit")),
67 | },
68 | }
69 | if err := file.GetDb().NewClient(t); err != nil {
70 | s.AjaxErr(err.Error())
71 | }
72 | s.AjaxOk("add success")
73 | }
74 | }
75 | func (s *ClientController) GetClient() {
76 | if s.Ctx.Request.Method == "POST" {
77 | id := s.GetIntNoErr("id")
78 | data := make(map[string]interface{})
79 | if c, err := file.GetDb().GetClient(id); err != nil {
80 | data["code"] = 0
81 | } else {
82 | data["code"] = 1
83 | data["data"] = c
84 | }
85 | s.Data["json"] = data
86 | s.ServeJSON()
87 | }
88 | }
89 |
90 | //修改客户端
91 | func (s *ClientController) Edit() {
92 | id := s.GetIntNoErr("id")
93 | if s.Ctx.Request.Method == "GET" {
94 | s.Data["menu"] = "client"
95 | if c, err := file.GetDb().GetClient(id); err != nil {
96 | s.error()
97 | } else {
98 | s.Data["c"] = c
99 | }
100 | s.SetInfo("edit client")
101 | s.display()
102 | } else {
103 | if c, err := file.GetDb().GetClient(id); err != nil {
104 | s.error()
105 | s.AjaxErr("client ID not found")
106 | return
107 | } else {
108 | if s.getEscapeString("web_username") != "" {
109 | if s.getEscapeString("web_username") == beego.AppConfig.String("web_username") || !file.GetDb().VerifyUserName(s.getEscapeString("web_username"), c.Id) {
110 | s.AjaxErr("web login username duplicate, please reset")
111 | return
112 | }
113 | }
114 | if s.GetSession("isAdmin").(bool) {
115 | if !file.GetDb().VerifyVkey(s.getEscapeString("vkey"), c.Id) {
116 | s.AjaxErr("Vkey duplicate, please reset")
117 | return
118 | }
119 | c.VerifyKey = s.getEscapeString("vkey")
120 | c.Flow.FlowLimit = int64(s.GetIntNoErr("flow_limit"))
121 | c.RateLimit = s.GetIntNoErr("rate_limit")
122 | c.MaxConn = s.GetIntNoErr("max_conn")
123 | c.MaxTunnelNum = s.GetIntNoErr("max_tunnel")
124 | }
125 | c.Remark = s.getEscapeString("remark")
126 | c.Cnf.U = s.getEscapeString("u")
127 | c.Cnf.P = s.getEscapeString("p")
128 | c.Cnf.Compress = common.GetBoolByStr(s.getEscapeString("compress"))
129 | c.Cnf.Crypt = s.GetBoolNoErr("crypt")
130 | b, err := beego.AppConfig.Bool("allow_user_change_username")
131 | if s.GetSession("isAdmin").(bool) || (err == nil && b) {
132 | c.WebUserName = s.getEscapeString("web_username")
133 | }
134 | c.WebPassword = s.getEscapeString("web_password")
135 | c.ConfigConnAllow = s.GetBoolNoErr("config_conn_allow")
136 | if c.Rate != nil {
137 | c.Rate.Stop()
138 | }
139 | if c.RateLimit > 0 {
140 | c.Rate = rate.NewRate(int64(c.RateLimit * 1024))
141 | c.Rate.Start()
142 | } else {
143 | c.Rate = rate.NewRate(int64(2 << 23))
144 | c.Rate.Start()
145 | }
146 | file.GetDb().JsonDb.StoreClientsToJsonFile()
147 | }
148 | s.AjaxOk("save success")
149 | }
150 | }
151 |
152 | //更改状态
153 | func (s *ClientController) ChangeStatus() {
154 | id := s.GetIntNoErr("id")
155 | if client, err := file.GetDb().GetClient(id); err == nil {
156 | client.Status = s.GetBoolNoErr("status")
157 | if client.Status == false {
158 | server.DelClientConnect(client.Id)
159 | }
160 | s.AjaxOk("modified success")
161 | }
162 | s.AjaxErr("modified fail")
163 | }
164 |
165 | //删除客户端
166 | func (s *ClientController) Del() {
167 | id := s.GetIntNoErr("id")
168 | if err := file.GetDb().DelClient(id); err != nil {
169 | s.AjaxErr("delete error")
170 | }
171 | server.DelTunnelAndHostByClientId(id, false)
172 | server.DelClientConnect(id)
173 | s.AjaxOk("delete success")
174 | }
175 |
--------------------------------------------------------------------------------
/web/controllers/login.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "math/rand"
5 | "net"
6 | "sync"
7 | "time"
8 |
9 | "ehang.io/nps/lib/common"
10 | "ehang.io/nps/lib/file"
11 | "ehang.io/nps/server"
12 | "github.com/astaxie/beego"
13 | )
14 |
15 | type LoginController struct {
16 | beego.Controller
17 | }
18 |
19 | var ipRecord sync.Map
20 |
21 | type record struct {
22 | hasLoginFailTimes int
23 | lastLoginTime time.Time
24 | }
25 |
26 | func (self *LoginController) Index() {
27 | // Try login implicitly, will succeed if it's configured as no-auth(empty username&password).
28 | webBaseUrl := beego.AppConfig.String("web_base_url")
29 | if self.doLogin("", "", false) {
30 | self.Redirect(webBaseUrl+"/index/index", 302)
31 | }
32 | self.Data["web_base_url"] = webBaseUrl
33 | self.Data["register_allow"], _ = beego.AppConfig.Bool("allow_user_register")
34 | self.TplName = "login/index.html"
35 | }
36 |
37 | func (self *LoginController) Verify() {
38 | username := self.GetString("username")
39 | password := self.GetString("password")
40 | if self.doLogin(username, password, true) {
41 | self.Data["json"] = map[string]interface{}{"status": 1, "msg": "login success"}
42 | } else {
43 | self.Data["json"] = map[string]interface{}{"status": 0, "msg": "username or password incorrect"}
44 | }
45 | self.ServeJSON()
46 | }
47 |
48 | func (self *LoginController) doLogin(username, password string, explicit bool) bool {
49 | clearIprecord()
50 | ip, _, _ := net.SplitHostPort(self.Ctx.Request.RemoteAddr)
51 | if v, ok := ipRecord.Load(ip); ok {
52 | vv := v.(*record)
53 | if (time.Now().Unix() - vv.lastLoginTime.Unix()) >= 60 {
54 | vv.hasLoginFailTimes = 0
55 | }
56 | if vv.hasLoginFailTimes >= 10 {
57 | return false
58 | }
59 | }
60 | var auth bool
61 | if password == beego.AppConfig.String("web_password") && username == beego.AppConfig.String("web_username") {
62 | self.SetSession("isAdmin", true)
63 | self.DelSession("clientId")
64 | self.DelSession("username")
65 | auth = true
66 | server.Bridge.Register.Store(common.GetIpByAddr(self.Ctx.Input.IP()), time.Now().Add(time.Hour*time.Duration(2)))
67 | }
68 | b, err := beego.AppConfig.Bool("allow_user_login")
69 | if err == nil && b && !auth {
70 | file.GetDb().JsonDb.Clients.Range(func(key, value interface{}) bool {
71 | v := value.(*file.Client)
72 | if !v.Status || v.NoDisplay {
73 | return true
74 | }
75 | if v.WebUserName == "" && v.WebPassword == "" {
76 | if username != "user" || v.VerifyKey != password {
77 | return true
78 | } else {
79 | auth = true
80 | }
81 | }
82 | if !auth && v.WebPassword == password && v.WebUserName == username {
83 | auth = true
84 | }
85 | if auth {
86 | self.SetSession("isAdmin", false)
87 | self.SetSession("clientId", v.Id)
88 | self.SetSession("username", v.WebUserName)
89 | return false
90 | }
91 | return true
92 | })
93 | }
94 | if auth {
95 | self.SetSession("auth", true)
96 | ipRecord.Delete(ip)
97 | return true
98 |
99 | }
100 | if v, load := ipRecord.LoadOrStore(ip, &record{hasLoginFailTimes: 1, lastLoginTime: time.Now()}); load && explicit {
101 | vv := v.(*record)
102 | vv.lastLoginTime = time.Now()
103 | vv.hasLoginFailTimes += 1
104 | ipRecord.Store(ip, vv)
105 | }
106 | return false
107 | }
108 | func (self *LoginController) Register() {
109 | if self.Ctx.Request.Method == "GET" {
110 | self.Data["web_base_url"] = beego.AppConfig.String("web_base_url")
111 | self.TplName = "login/register.html"
112 | } else {
113 | if b, err := beego.AppConfig.Bool("allow_user_register"); err != nil || !b {
114 | self.Data["json"] = map[string]interface{}{"status": 0, "msg": "register is not allow"}
115 | self.ServeJSON()
116 | return
117 | }
118 | if self.GetString("username") == "" || self.GetString("password") == "" || self.GetString("username") == beego.AppConfig.String("web_username") {
119 | self.Data["json"] = map[string]interface{}{"status": 0, "msg": "please check your input"}
120 | self.ServeJSON()
121 | return
122 | }
123 | t := &file.Client{
124 | Id: int(file.GetDb().JsonDb.GetClientId()),
125 | Status: true,
126 | Cnf: &file.Config{},
127 | WebUserName: self.GetString("username"),
128 | WebPassword: self.GetString("password"),
129 | Flow: &file.Flow{},
130 | }
131 | if err := file.GetDb().NewClient(t); err != nil {
132 | self.Data["json"] = map[string]interface{}{"status": 0, "msg": err.Error()}
133 | } else {
134 | self.Data["json"] = map[string]interface{}{"status": 1, "msg": "register success"}
135 | }
136 | self.ServeJSON()
137 | }
138 | }
139 |
140 | func (self *LoginController) Out() {
141 | self.SetSession("auth", false)
142 | self.Redirect(beego.AppConfig.String("web_base_url")+"/login/index", 302)
143 | }
144 |
145 | func clearIprecord() {
146 | rand.Seed(time.Now().UnixNano())
147 | x := rand.Intn(100)
148 | if x == 1 {
149 | ipRecord.Range(func(key, value interface{}) bool {
150 | v := value.(*record)
151 | if time.Now().Unix()-v.lastLoginTime.Unix() >= 60 {
152 | ipRecord.Delete(key)
153 | }
154 | return true
155 | })
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/web/routers/router.go:
--------------------------------------------------------------------------------
1 | package routers
2 |
3 | import (
4 | "ehang.io/nps/web/controllers"
5 | "github.com/astaxie/beego"
6 | )
7 |
8 | func Init() {
9 | web_base_url := beego.AppConfig.String("web_base_url")
10 | if len(web_base_url) > 0 {
11 | ns := beego.NewNamespace(web_base_url,
12 | beego.NSRouter("/", &controllers.IndexController{}, "*:Index"),
13 | beego.NSAutoRouter(&controllers.IndexController{}),
14 | beego.NSAutoRouter(&controllers.LoginController{}),
15 | beego.NSAutoRouter(&controllers.ClientController{}),
16 | beego.NSAutoRouter(&controllers.AuthController{}),
17 | )
18 | beego.AddNamespace(ns)
19 | } else {
20 | beego.Router("/", &controllers.IndexController{}, "*:Index")
21 | beego.AutoRouter(&controllers.IndexController{})
22 | beego.AutoRouter(&controllers.LoginController{})
23 | beego.AutoRouter(&controllers.ClientController{})
24 | beego.AutoRouter(&controllers.AuthController{})
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/web/static/css/regular.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 5.11.2 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | */
5 | @font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:auto;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:"Font Awesome 5 Free";font-weight:400}
--------------------------------------------------------------------------------
/web/static/css/solid.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 5.11.2 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | */
5 | @font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:auto;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900}
--------------------------------------------------------------------------------
/web/static/img/flag/en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/web/static/img/flag/en-US.png
--------------------------------------------------------------------------------
/web/static/img/flag/zh-CN.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/web/static/img/flag/zh-CN.png
--------------------------------------------------------------------------------
/web/static/js/language.js:
--------------------------------------------------------------------------------
1 | (function ($) {
2 |
3 | function xml2json(Xml) {
4 | var tempvalue, tempJson = {};
5 | $(Xml).each(function() {
6 | var tagName = ($(this).attr('id') || this.tagName);
7 | tempvalue = (this.childElementCount == 0) ? this.textContent : xml2json($(this).children());
8 | switch ($.type(tempJson[tagName])) {
9 | case 'undefined':
10 | tempJson[tagName] = tempvalue;
11 | break;
12 | case 'object':
13 | tempJson[tagName] = Array(tempJson[tagName]);
14 | case 'array':
15 | tempJson[tagName].push(tempvalue);
16 | }
17 | });
18 | return tempJson;
19 | }
20 |
21 | function setCookie (c_name, value, expiredays) {
22 | var exdate = new Date();
23 | exdate.setDate(exdate.getDate() + expiredays);
24 | document.cookie = c_name + '=' + escape(value) + ((expiredays == null) ? '' : ';expires=' + exdate.toGMTString())+ '; path='+window.nps.web_base_url+'/;';
25 | }
26 |
27 | function getCookie (c_name) {
28 | if (document.cookie.length > 0) {
29 | c_start = document.cookie.indexOf(c_name + '=');
30 | if (c_start != -1) {
31 | c_start = c_start + c_name.length + 1;
32 | c_end = document.cookie.indexOf(';', c_start);
33 | if (c_end == -1) c_end = document.cookie.length;
34 | return unescape(document.cookie.substring(c_start, c_end));
35 | }
36 | }
37 | return null;
38 | }
39 |
40 | function setchartlang (langobj,chartobj) {
41 | if ( $.type (langobj) == 'string' ) return langobj;
42 | if ( $.type (langobj) == 'chartobj' ) return false;
43 | var flag = true;
44 | for (key in langobj) {
45 | var item = key;
46 | children = (chartobj.hasOwnProperty(item)) ? setchartlang (langobj[item],chartobj[item]) : setchartlang (langobj[item],undefined);
47 | switch ($.type(children)) {
48 | case 'string':
49 | if ($.type(chartobj[item]) != 'string' ) continue;
50 | case 'object':
51 | chartobj[item] = (children['value'] || children);
52 | default:
53 | flag = false;
54 | }
55 | }
56 | if (flag) { return {'value':(langobj[languages['current']] || langobj[languages['default']] || 'N/A')}}
57 | }
58 |
59 | $.fn.cloudLang = function () {
60 | $.ajax({
61 | type: 'GET',
62 | url: window.nps.web_base_url + '/static/page/languages.xml',
63 | dataType: 'xml',
64 | success: function (xml) {
65 | languages['content'] = xml2json($(xml).children())['content'];
66 | languages['menu'] = languages['content']['languages'];
67 | languages['default'] = languages['content']['default'];
68 | languages['navigator'] = (getCookie ('lang') || navigator.language || navigator.browserLanguage);
69 | for(var key in languages['menu']){
70 | $('#languagemenu').next().append('
' + languages['menu'][key] +'');
71 | if ( key == languages['navigator'] ) languages['current'] = key;
72 | }
73 | $('#languagemenu').attr('lang',(languages['current'] || languages['default']));
74 | $('body').setLang ('');
75 | }
76 | });
77 | };
78 |
79 | $.fn.setLang = function (dom) {
80 | languages['current'] = $('#languagemenu').attr('lang');
81 | if ( dom == '' ) {
82 | $('#languagemenu span').text(' ' + languages['menu'][languages['current']]);
83 | if (languages['current'] != getCookie('lang')) setCookie('lang', languages['current']);
84 | if($("#table").length>0) $('#table').bootstrapTable('refreshOptions', { 'locale': languages['current']});
85 | }
86 | $.each($(dom + ' [langtag]'), function (i, item) {
87 | var index = $(item).attr('langtag');
88 | string = languages['content'][index.toLowerCase()];
89 | switch ($.type(string)) {
90 | case 'string':
91 | break;
92 | case 'array':
93 | string = string[Math.floor((Math.random()*string.length))];
94 | case 'object':
95 | string = (string[languages['current']] || string[languages['default']] || null);
96 | break;
97 | default:
98 | string = 'Missing language string "' + index + '"';
99 | $(item).css('background-color','#ffeeba');
100 | }
101 | if($.type($(item).attr('placeholder')) == 'undefined') {
102 | $(item).text(string);
103 | } else {
104 | $(item).attr('placeholder', string);
105 | }
106 | });
107 |
108 | if ( !$.isEmptyObject(chartdatas) ) {
109 | setchartlang(languages['content']['charts'],chartdatas);
110 | for(var key in chartdatas){
111 | if ($('#'+key).length == 0) continue;
112 | if($.type(chartdatas[key]) == 'object')
113 | charts[key] = echarts.init(document.getElementById(key));
114 | charts[key].setOption(chartdatas[key], true);
115 | }
116 | }
117 | }
118 |
119 | })(jQuery);
120 |
121 | $(document).ready(function () {
122 | $('body').cloudLang();
123 | $('body').on('click','li[lang]',function(){
124 | $('#languagemenu').attr('lang',$(this).attr('lang'));
125 | $('body').setLang ('');
126 | });
127 | });
128 |
129 | var languages = {};
130 | var charts = {};
131 | var chartdatas = {};
132 | var postsubmit;
133 |
134 | function langreply(langstr) {
135 | var langobj = languages['content']['reply'][langstr.replace(/[\s,\.\?]*/g,"").toLowerCase()];
136 | if ($.type(langobj) == 'undefined') return langstr
137 | langobj = (langobj[languages['current']] || langobj[languages['default']] || langstr);
138 | return langobj
139 | }
140 |
141 | function submitform(action, url, postdata) {
142 | postsubmit = false;
143 | switch (action) {
144 | case 'start':
145 | case 'stop':
146 | case 'delete':
147 | var langobj = languages['content']['confirm'][action];
148 | action = (langobj[languages['current']] || langobj[languages['default']] || 'Are you sure you want to ' + action + ' it?');
149 | if (! confirm(action)) return;
150 | postsubmit = true;
151 | case 'add':
152 | case 'edit':
153 | $.ajax({
154 | type: "POST",
155 | url: url,
156 | data: postdata,
157 | success: function (res) {
158 | alert(langreply(res.msg));
159 | if (res.status) {
160 | if (postsubmit) {document.location.reload();}else{history.back(-1);}
161 | }
162 | }
163 | });
164 | }
165 | }
166 |
167 | function changeunit(limit) {
168 | var size = "";
169 | if (limit < 0.1 * 1024) {
170 | size = limit.toFixed(2) + "B";
171 | } else if (limit < 0.1 * 1024 * 1024) {
172 | size = (limit / 1024).toFixed(2) + "KB";
173 | } else if (limit < 0.1 * 1024 * 1024 * 1024) {
174 | size = (limit / (1024 * 1024)).toFixed(2) + "MB";
175 | } else {
176 | size = (limit / (1024 * 1024 * 1024)).toFixed(2) + "GB";
177 | }
178 |
179 | var sizeStr = size + "";
180 | var index = sizeStr.indexOf(".");
181 | var dou = sizeStr.substr(index + 1, 2);
182 | if (dou == "00") {
183 | return sizeStr.substring(0, index) + sizeStr.substr(index + 3, 2);
184 | }
185 | return size;
186 | }
--------------------------------------------------------------------------------
/web/static/page/error.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | nps error
6 |
7 |
8 | 404 not found,power by nps
9 |
10 |
--------------------------------------------------------------------------------
/web/static/webfonts/fa-solid-900.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/web/static/webfonts/fa-solid-900.eot
--------------------------------------------------------------------------------
/web/static/webfonts/fa-solid-900.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/web/static/webfonts/fa-solid-900.ttf
--------------------------------------------------------------------------------
/web/static/webfonts/fa-solid-900.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/web/static/webfonts/fa-solid-900.woff
--------------------------------------------------------------------------------
/web/static/webfonts/fa-solid-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ehang-io/nps/ab648d6f0c618c690a7a79948a7ebd686e1cdafc/web/static/webfonts/fa-solid-900.woff2
--------------------------------------------------------------------------------
/web/views/index/hadd.html:
--------------------------------------------------------------------------------
1 |
101 |
--------------------------------------------------------------------------------
/web/views/index/help.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
8 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
域名代理模式
18 |
19 | 适用范围: 小程序开发、微信公众号开发、产品演示
20 |
21 |
22 | 假设场景:
23 |
有一个域名proxy.com,有一台公网机器ip为{{.ip}}
24 |
两个内网开发站点127.0.0.1:81,127.0.0.1:82
25 |
想通过a.proxy.com访问127.0.0.1:81,通过b.proxy.com访问127.0.0.1:82
26 |
27 |
使用步骤:
28 |
29 | - 将*.proxy.com解析到公网服务器{{.ip}}
30 | - 在客户端管理中创建一个客户端,记录下验证密钥
31 | - 点击该客户端的域名管理,添加两条规则规则:1、域名:a.proxy.com,内网目标:127.0.0.1:81,2、域名:b.proxy.com,内网目标:127.0.0.1:82
32 | - 内网客户端运行
33 | ./npc -server={{.ip}}:{{.p}} -vkey=客户端的密钥
34 |
35 | - 现在访问a.proxy.com,b.proxy.com即可成功
36 |
37 |
注:上文中提到公网ip({{.ip}})为系统自动识别,如果是在测试环境中请自行对应,如需使用https请在配置文件中将https端口设置为443,和将对应的证书文件路径添加到配置文件中
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
tcp隧道模式
46 |
47 | 适用范围: ssh、远程桌面等tcp连接场景
48 |
49 |
50 | 假设场景: 想通过访问公网服务器{{.ip}}的8001端口,连接内网机器10.1.50.101的22端口,实现ssh连接
51 |
52 |
使用步骤:
53 |
54 | - 在客户端管理中创建一个客户端,记录下验证密钥
55 | - 内网客户端运行
56 | ./npc -server={{.ip}}:{{.p}} -vkey=客户端的密钥
57 |
58 |
59 | - 在该客户端隧道管理中添加一条tcp隧道,填写监听的端口(8001)、内网目标ip和目标端口(10.1.50.101:22),选择压缩方式,保存。
60 | - 访问公网服务器ip({{.ip}}),填写的监听端口(8001),相当于访问内网ip(10.1.50.101):目标端口(22),例如:ssh -p 8001 root@{{.ip}}
61 |
62 |
注:上文中提到公网ip({{.ip}})为系统自动识别,如果是在测试环境中请自行对应,默认内网客户端已经启动
63 |
64 |
65 |
66 |
67 |
udp隧道模式
68 |
69 | 适用范围: 内网dns解析等udp连接场景
70 |
71 |
72 | 假设场景: 内网有一台dns(10.1.50.102:53),在非内网环境下想使用该dns,公网服务器为{{.ip}}
73 |
74 |
使用步骤:
75 |
76 | - 在客户端管理中创建一个客户端,记录下验证密钥
77 | - 内网客户端运行
78 | ./npc -server={{.ip}}:{{.p}} -vkey=客户端的密钥
79 |
80 |
81 | - 在该客户端的隧道管理中添加一条udp隧道,填写监听的端口(53)、内网目标ip和目标端口(10.1.50.102:53),选择压缩方式,保存。
82 | - 修改本机dns为{{.ip}},则相当于使用10.1.50.202作为dns服务器
83 |
84 |
注:上文中提到公网ip({{.ip}})为系统自动识别,如果是在测试环境中请自行对应,默认内网客户端已经启动
85 |
86 |
87 |
88 |
89 |
90 |
91 |
socks5代理模式
92 |
93 | 适用范围: 在外网环境下如同使用vpn一样访问内网设备或者资源
94 |
95 |
96 | 假设场景: 想将公网服务器{{.ip}}的8003端口作为socks5代理,达到访问内网任意设备或者资源的效果
97 |
98 |
使用步骤:
99 |
100 | - 在客户端管理中创建一个客户端,记录下验证密钥
101 | - 内网客户端运行
102 | ./npc -server={{.ip}}:{{.p}} -vkey=客户端的密钥
103 |
104 |
105 | - 在该客户端隧道管理中添加一条socks5代理,填写监听的端口(8003),验证用户名和密码自行选择(建议先不填,部分客户端不支持,proxifer支持),选择压缩方式,保存。
106 | - 在外网环境的本机配置socks5代理,ip为公网服务器ip({{.ip}}),端口为填写的监听端口(8003),即可畅享内网了
107 |
108 |
注:上文中提到公网ip({{.ip}})为系统自动识别,如果是在测试环境中请自行对应,默认内网客户端已经启动
109 |
110 |
111 |
112 |
113 |
http代理模式
114 |
115 | 适用范围: 在外网环境下访问内网站点
116 |
117 |
118 | 假设场景: 想将公网服务器{{.ip}}的8004端口作为http代理,访问内网网站
119 |
120 |
使用步骤:
121 |
122 | - 在客户端管理中创建一个客户端,记录下验证密钥
123 | - 内网客户端运行
124 | ./npc -server={{.ip}}:{{.p}} -vkey=客户端的密钥
125 |
126 |
127 | - 在该客户端隧道管理中添加一条http代理,填写监听的端口(8004),选择压缩方式,保存。
128 | - 在外网环境的本机配置http代理,ip为公网服务器ip({{.ip}}),端口为填写的监听端口(8004),即可访问了
129 |
130 |
注:上文中提到公网ip({{.ip}})为系统自动识别,如果是在测试环境中请自行对应,默认内网客户端已经启动
131 |
132 |
133 |
134 |
135 |
单个客户端可以添加多条隧道或者域名解析
136 |
137 |
138 |
139 |
--------------------------------------------------------------------------------
/web/views/login/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
72 |
73 |
74 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
105 |