├── .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 | ![](https://img.shields.io/github/stars/ehang-io/nps.svg) ![](https://img.shields.io/github/forks/ehang-io/nps.svg) 4 | [![Gitter](https://badges.gitter.im/cnlh-nps/community.svg)](https://gitter.im/cnlh-nps/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 5 | ![Release](https://github.com/ehang-io/nps/workflows/Release/badge.svg) 6 | ![GitHub All Releases](https://img.shields.io/github/downloads/ehang-io/nps/total) 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 | ![image](https://github.com/ehang-io/nps/blob/master/image/web.png?raw=true) 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 | ![](https://img.shields.io/github/stars/ehang-io/nps.svg) ![](https://img.shields.io/github/forks/ehang-io/nps.svg) 4 | [![Gitter](https://badges.gitter.im/cnlh-nps/community.svg)](https://gitter.im/cnlh-nps/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 5 | ![Release](https://github.com/ehang-io/nps/workflows/Release/badge.svg) 6 | ![GitHub All Releases](https://img.shields.io/github/downloads/ehang-io/nps/total) 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 | ![image](https://github.com/ehang-io/nps/blob/master/image/web.png?raw=true) 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 | ![](https://img.shields.io/github/stars/cnlh/nps.svg) ![](https://img.shields.io/github/forks/cnlh/nps.svg) 3 | [![Gitter](https://badges.gitter.im/cnlh-nps/community.svg)](https://gitter.im/cnlh-nps/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 4 | [![Build Status](https://travis-ci.org/ehang-io/nps.svg?branch=master)](https://travis-ci.org/cnlh/nps) 5 | 6 | nps是一款轻量级、高性能、功能强大的**内网穿透**代理服务器。目前支持**tcp、udp流量转发**,可支持任何**tcp、udp**上层协议(访问内网网站、本地支付接口调试、ssh访问、远程桌面,内网dns解析等等……),此外还**支持内网http代理、内网socks5代理**、**p2p等**,并带有功能强大的web管理端。 7 | 8 | 9 | ## 背景 10 | ![image](https://github.com/ehang-io/nps/blob/master/image/web.png?raw=true) 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 | ![logo](logo.svg) 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 | * [![GitHub stars](https://img.shields.io/github/stars/ehang-io/nps?style=social)](https://github.com/ehang-io/nps/stargazers) 2 | 3 | * [![GitHub forks](https://img.shields.io/github/forks/ehang-io/nps?style=social)](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 | ![二维码.jpeg](https://i.loli.net/2019/02/15/5c66c32a42074.jpeg) 4 | -------------------------------------------------------------------------------- /docs/donate.md: -------------------------------------------------------------------------------- 1 | # 捐助 2 | 如果您觉得nps对你有帮助,欢迎给予我们一定捐助,也是帮助nps更好的发展。 3 | 4 | ## 支付宝 5 | ![image](https://github.com/ehang-io/nps/blob/master/image/donation_zfb.png?raw=true) 6 | ## 微信 7 | ![image](https://github.com/ehang-io/nps/blob/master/image/donation_wx.png?raw=true) 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 | ![image](https://github.com/ehang-io/nps/blob/master/image/web2.png?raw=true) 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 | ![image](https://github.com/ehang-io/nps/blob/master/docs/windows_client_service_configuration.png?raw=true) 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 |
    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 | 33 |
    34 |
    35 | {{if eq false .https_just_proxy}} 36 |
    37 | 38 |
    39 | 40 |
    41 |
    42 |
    43 | 44 |
    45 | 46 |
    47 |
    48 | {{end}} 49 |
    50 | 51 |
    52 | 53 |
    54 |
    55 | {{if eq true .allow_local_proxy}} 56 |
    57 | 58 |
    59 | 63 |
    64 |
    65 | {{end}} 66 |
    67 | 68 |
    69 | 70 | 71 | 72 |
    73 |
    74 | 82 |
    83 | 84 |
    85 | 86 |
    87 |
    88 |
    89 |
    90 |
    91 | 94 |
    95 |
    96 |
    97 |
    98 |
    99 |
    100 |
    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 |
    56 |
    57 |
    58 |
    59 | 60 |
    61 |
    62 | 63 |
    64 | 65 | {{if eq true .register_allow}} 66 |

    67 | 68 | {{end}} 69 |
    70 |
    71 |
    72 |
    73 |
    74 | 80 |
    81 | 82 | 83 | 84 | 85 | 86 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /web/views/login/register.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 | 35 |
    36 |
    37 |
    38 |

    39 |
    40 |

    41 |

    42 |
    43 |
    44 | 45 |
    46 |
    47 | 48 |
    49 | 50 |

    51 | 52 |
    53 |
    54 |
    55 | 61 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /web/views/public/error.html: -------------------------------------------------------------------------------- 1 |
    2 |

    Error 404: Page not found

    3 |

    The page you have requested is not found.

    4 |

    Go Back

    5 |
    --------------------------------------------------------------------------------