├── .codecov.yml ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── 00-question.md └── workflows │ ├── codeql-analysis.yml │ ├── go-update.yml │ ├── go.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── build.sh ├── build_docker.sh ├── build_test.sh ├── build_web.sh ├── deploy ├── anylink.service ├── deployment.yaml └── docker-compose.yaml ├── deploy_docker_cn.sh ├── doc ├── README.md ├── question.md └── screenshot │ ├── group.jpg │ ├── ip_map.jpg │ ├── jetbrains.png │ ├── online.jpg │ ├── qq.png │ ├── qq2.jpg │ ├── setting.jpg │ ├── system.jpg │ ├── users.jpg │ ├── wxpay.png │ └── wxpay2.png ├── docker ├── Dockerfile ├── docker_entrypoint.sh ├── generate-certs.sh ├── init_build.sh └── init_release.sh ├── index_template ├── 自定义首页1.html └── 自定义首页2.html ├── release.sh ├── server ├── .codecov.yml ├── .gitignore ├── admin │ ├── api_base.go │ ├── api_cert.go │ ├── api_group.go │ ├── api_ip_map.go │ ├── api_other.go │ ├── api_policy.go │ ├── api_set.go │ ├── api_set_audit.go │ ├── api_statsinfo.go │ ├── api_uploaduser.go │ ├── api_user.go │ ├── common.go │ ├── common_test.go │ ├── error.go │ ├── lockmanager.go │ ├── resp.go │ ├── resp_test.go │ └── server.go ├── base │ ├── app_ver.go │ ├── cfg.go │ ├── cmd.go │ ├── config.go │ ├── log.go │ ├── mod.go │ └── start.go ├── bridge-init.sh ├── conf │ ├── files │ │ ├── index.html │ │ └── info.txt │ ├── profile.xml │ ├── server-sample.toml │ ├── server.toml │ ├── vpn_cert.key │ └── vpn_cert.pem ├── cron │ ├── clear_audit.go │ ├── clear_statsinfo.go │ ├── clear_user_act_log.go │ └── start.go ├── dbdata │ ├── audit.go │ ├── audit_test.go │ ├── cert.go │ ├── db.go │ ├── db_orm.go │ ├── db_test.go │ ├── group.go │ ├── group_test.go │ ├── ip_map.go │ ├── policy.go │ ├── policy_test.go │ ├── setting.go │ ├── start.go │ ├── statsinfo.go │ ├── statsinfo_test.go │ ├── tables.go │ ├── user.go │ ├── user_act_log.go │ ├── user_act_log_test.go │ ├── user_test.go │ ├── userauth.go │ ├── userauth_ldap.go │ └── userauth_radius.go ├── go.mod ├── go.sum ├── handler │ ├── dtls.go │ ├── link_auth.go │ ├── link_auth_otp.go │ ├── link_base.go │ ├── link_cstp.go │ ├── link_dtls.go │ ├── link_home.go │ ├── link_tap.go │ ├── link_tun.go │ ├── link_tunnel.go │ ├── link_vtap.go │ ├── payload.go │ ├── payload_access_audit.go │ ├── payload_tcp_parser.go │ ├── payload_test.go │ ├── pool.go │ ├── pool_test.go │ ├── server.go │ └── start.go ├── main.go ├── pkg │ ├── arpdis │ │ ├── addr.go │ │ ├── addr_test.go │ │ ├── arp.go │ │ ├── icmp.go │ │ └── lookup.go │ └── utils │ │ ├── ip.go │ │ ├── maps.go │ │ ├── maps_test.go │ │ ├── password_hash.go │ │ ├── secure_header.go │ │ ├── unsafe.go │ │ ├── util.go │ │ └── util_test.go └── sessdata │ ├── compress.go │ ├── compress_test.go │ ├── copy_struct.go │ ├── copy_struct_test.go │ ├── ip_pool.go │ ├── ip_pool_test.go │ ├── limit_client.go │ ├── limit_rate.go │ ├── limit_test.go │ ├── online.go │ ├── protocol.go │ ├── session.go │ ├── session_test.go │ ├── start.go │ └── statsinfo.go ├── version ├── version_info └── web ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── package.json ├── public ├── favicon.ico ├── index.html └── 批量添加用户模版.xlsx ├── src ├── App.vue ├── assets │ └── logo.png ├── components │ ├── Cell.vue │ ├── LineChart.vue │ └── audit │ │ ├── Access.vue │ │ └── ActLog.vue ├── layout │ ├── Layout.vue │ ├── LayoutAside.vue │ └── LayoutHeader.vue ├── main.js ├── pages │ ├── Home.vue │ ├── Login.vue │ ├── group │ │ └── List.vue │ ├── set │ │ ├── Audit.vue │ │ ├── Other.vue │ │ ├── Soft.vue │ │ └── System.vue │ └── user │ │ ├── IpMap.vue │ │ ├── List.vue │ │ ├── LockManager.vue │ │ ├── Online.vue │ │ └── Policy.vue └── plugins │ ├── element.js │ ├── mixin.js │ ├── request.js │ ├── router.js │ └── token.js ├── vue.config.js └── yarn.lock /.codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "^doc" 3 | - "^home" 4 | - "^web" 5 | - "^server/conf" 6 | - "^server/files" 7 | 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: [ 'https://github.com/bjdgyc/anylink/blob/main/doc/README.md' ] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/00-question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 问题描述 3 | about: 问题描述 4 | title: "affected/package: " 5 | --- 6 | 7 | 10 | 11 | ### 使用的anylink版本 ? 12 | 13 |
14 | ./anylink tool -v
15 | 管理后台也可以查看
16 | 
17 | 18 | 19 | ### 使用操作系统的类型和版本? 20 | 如: centos 7.9 21 |
22 | cat /etc/issue
23 | cat /etc/redhat-release
24 | 
25 | 
26 | 27 | ### 使用linux 内核版本? 28 |
29 | uname -a
30 | 
31 | 
32 | 33 | ### 具体遇到的问题,可上传截图 34 | 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | workflow_dispatch: 16 | schedule: 17 | - cron: '32 5 * * 1' 18 | # push: 19 | # branches: [ "main", "dev" ] 20 | # pull_request: 21 | # branches: [ "main", "dev" ] 22 | 23 | 24 | 25 | jobs: 26 | analyze: 27 | name: Analyze 28 | runs-on: ubuntu-latest 29 | permissions: 30 | actions: read 31 | contents: read 32 | security-events: write 33 | 34 | strategy: 35 | fail-fast: false 36 | matrix: 37 | language: [ 'go', 'javascript' ] 38 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 39 | # Learn more: 40 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 41 | 42 | steps: 43 | - name: Checkout repository 44 | uses: actions/checkout@v4 45 | 46 | # Initializes the CodeQL tools for scanning. 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@v2 49 | with: 50 | languages: ${{ matrix.language }} 51 | # If you wish to specify custom queries, you can do so here or in a config file. 52 | # By default, queries listed here will override any specified in a config file. 53 | # Prefix the list here with "+" to use these queries and those in the config file. 54 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 https://git.io/JvXDl 63 | 64 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 65 | # and modify them (or add more) to build your code if your project 66 | # uses a compiled language 67 | 68 | #- run: | 69 | # make bootstrap 70 | # make release 71 | 72 | - name: Perform CodeQL Analysis 73 | uses: github/codeql-action/analyze@v2 74 | -------------------------------------------------------------------------------- /.github/workflows/go-update.yml: -------------------------------------------------------------------------------- 1 | name: go-update 2 | on: 3 | workflow_dispatch: 4 | # schedule: 5 | # - cron: "1 2 * * 5" 6 | 7 | jobs: 8 | go: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Setup Go 1.x.y 12 | uses: actions/setup-go@main 13 | with: 14 | go-version: '1.20' 15 | stable: true 16 | 17 | - name: Checkout codebase 18 | uses: actions/checkout@main 19 | 20 | - name: go 21 | run: | 22 | cd ./server 23 | go mod tidy -compat=1.20 24 | #gofmt -w -r 'interface{} -> any' . 25 | go get -u 26 | go mod download 27 | go get -u 28 | go mod download 29 | 30 | - name: Git push 31 | run: | 32 | git init 33 | git config --local user.name "github-actions[bot]" 34 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 35 | git remote rm origin 36 | git remote add origin "https://${{ github.actor }}:${{ secrets.GITHUBTOKEN }}@github.com/${{ github.repository }}" 37 | git gc --aggressive 38 | git add --all 39 | git commit -m "update go.mod $(date +%Y.%m.%d.%H.%M)" 40 | #git push -f -u origin _autoaction 41 | git push -u origin _autoaction 42 | 43 | # 删除无用 workflow runs; 44 | - name: Delete workflow runs 45 | uses: GitRML/delete-workflow-runs@main 46 | with: 47 | retain_days: 0.1 48 | keep_minimum_runs: 1 49 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | push: 7 | branches: [ "main", "dev" ] 8 | pull_request: 9 | branches: [ "main", "dev" ] 10 | 11 | jobs: 12 | 13 | build: 14 | name: Build 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Check out code into the Go module directory 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Go 1.x 21 | uses: actions/setup-go@v4 22 | with: 23 | go-version: '1.22' 24 | go-version-file: 'server/go.mod' 25 | cache-dependency-path: 'server/go.sum' 26 | 27 | - name: Get dependencies 28 | run: | 29 | cd server 30 | go get -v -t -d ./... 31 | 32 | - name: Build 33 | run: | 34 | cd server 35 | mkdir ui 36 | touch ui/index.html 37 | go build -v -o anylink -trimpath -ldflags "-X main.CommitId=`git rev-parse HEAD`" 38 | ./anylink tool -v 39 | 40 | - name: Test coverage 41 | run: | 42 | cd server 43 | go test ./... 44 | go test -race -coverprofile=coverage.txt -covermode=atomic -v ./... 45 | 46 | - name: Upload coverage reports to Codecov 47 | uses: codecov/codecov-action@v3 48 | env: 49 | CODECOV_TOKEN: 28d52fb0-8fc9-460f-95b9-fb84f9138e58 50 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - "v0.*" 8 | - "v1.*" 9 | 10 | jobs: 11 | Build: 12 | name: build-binary 13 | runs-on: ubuntu-latest 14 | env: 15 | TZ: Asia/Shanghai 16 | steps: 17 | - name: Hello world 18 | run: uname -a 19 | 20 | - uses: actions/checkout@v4 21 | 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: '16' 25 | cache: 'yarn' 26 | cache-dependency-path: 'web/yarn.lock' 27 | - name: Build web 28 | working-directory: web 29 | run: | 30 | yarn install 31 | yarn run build 32 | # - uses: actions/setup-go@v4 33 | # with: 34 | # go-version: '1.20' 35 | # cache-dependency-path: 'server/go.sum' 36 | 37 | - name: Set up QEMU 38 | # https://github.com/docker/setup-qemu-action 39 | uses: docker/setup-qemu-action@v3 40 | - name: Set up Docker Buildx 41 | uses: docker/setup-buildx-action@v3 42 | 43 | - name: Login to Docker Hub 44 | uses: docker/login-action@v3 45 | with: 46 | username: bjdgyc 47 | password: ${{ secrets.DOCKERHUB_TOKEN }} 48 | logout: true 49 | 50 | - name: Pre bash 51 | shell: bash 52 | run: | 53 | appVer=`cat version` 54 | commitId=`git rev-parse HEAD` 55 | echo "APP_VER=$appVer" >> $GITHUB_ENV 56 | echo "commitId=$commitId" >> $GITHUB_ENV 57 | 58 | echo $appVer > version_info 59 | echo $commitId >> version_info 60 | echo $GITHUB_REF >> version_info 61 | echo $GITHUB_REF_NAME >> version_info 62 | 63 | #cd server;go mod tidy 64 | 65 | - name: Build and push 66 | uses: docker/build-push-action@v5 67 | with: 68 | push: true 69 | cache-from: type=gha,scope=anylink 70 | cache-to: type=gha,mode=max,scope=anylink 71 | context: . 72 | file: ./docker/Dockerfile 73 | platforms: linux/amd64,linux/arm64 74 | #platforms: linux/amd64 75 | build-args: | 76 | appVer=${{ env.APP_VER }} 77 | commitId=${{ env.commitId }} 78 | tags: bjdgyc/anylink:${{ env.APP_VER }},bjdgyc/anylink:latest 79 | #tags: bjdgyc/anylink:${{ env.APP_VER }} 80 | 81 | - name: Build deploy binary 82 | shell: bash 83 | run: bash release.sh 84 | 85 | - name: Release 86 | # https://github.com/ncipollo/release-action 87 | # artifacts: bin/release/* 88 | # generateReleaseNotes: true 89 | # draft: true 90 | # https://github.com/softprops/action-gh-release 91 | uses: softprops/action-gh-release@v1 92 | #if: startsWith(github.ref, 'refs/tags/') 93 | with: 94 | tag_name: v${{ env.APP_VER }} 95 | files: artifact-dist/* 96 | 97 | # Docker: 98 | # name: build-docker 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | .idea/ 3 | anylink-deploy 4 | anylink-deploy.tar.gz 5 | anylink-deploy-* 6 | anylink 7 | anylink.db 8 | 9 | dist 10 | artifact-dist 11 | 12 | anylink_amd64 13 | anylink_arm64 14 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | #https://goreleaser.com/static/schema.json 2 | 3 | # release --skip=publish 4 | 5 | # goreleaser build --skip=validate --clean --debug 6 | 7 | # GOPROXY=https://goproxy.cn 8 | 9 | # docker run -it --rm -v $PWD:/app -v /go:/go -w /app --platform=linux/arm64 golang:alpine3.19 10 | 11 | # docker run -it --rm -v $PWD:/app -v /go:/go -w /app goreleaser/goreleaser-cross build --skip=validate --clean --debug 12 | 13 | # docker run -it --rm -v $PWD:/app -v /go:/go -w /app bjdgyc/dcross goreleaser build --skip=validate --clean --debug 14 | 15 | version: 1 16 | 17 | dist: dist 18 | 19 | before: 20 | hooks: 21 | - pwd 22 | # - cmd: go mod tidy 23 | # dir: 24 | # "{{ dir .Dist}}" 25 | # output: true 26 | # - cmd: go generate 27 | # dir: 28 | # "{{ dir .Dist}}" 29 | # output: true 30 | 31 | builds: 32 | - id: "build" 33 | #main: . 34 | dir: ./server 35 | hooks: 36 | pre: 37 | - cmd: go mod tidy 38 | dir: ./server 39 | output: true 40 | - cmd: go generate 41 | dir: ./server 42 | output: true 43 | # {{- if eq .Arch "amd64" }}CC=x86_64-linux-gnu-gcc CXX=x86_64-linux-gnu-g++{{- end }} 44 | env: 45 | - CGO_ENABLED=1 46 | - >- 47 | {{- if eq .Os "linux" }} 48 | {{- if eq .Arch "amd64" }}CC=x86_64-linux-musl-gcc{{- end }} 49 | 50 | {{- if eq .Arch "arm64" }}CC=aarch64-linux-gnu-gcc{{- end }} 51 | {{- end }} 52 | {{- if eq .Os "darwin" }} 53 | {{- if eq .Arch "amd64"}}CC=o64-clang{{- end }} 54 | {{- if eq .Arch "arm64"}}CC=oa64-clang{{- end }} 55 | {{- end }} 56 | {{- if eq .Os "windows" }} 57 | {{- if eq .Arch "amd64"}}CC=x86_64-w64-mingw32-gcc{{- end }} 58 | {{- if eq .Arch "arm64"}}CC=aarch64-linux-gnu-gcc{{- end }} 59 | {{- end }} 60 | goos: 61 | - linux 62 | #- darwin 63 | #- windows 64 | goarch: 65 | - amd64 66 | #- arm64 67 | # https://go.dev/wiki/MinimumRequirements 68 | goamd64: 69 | - v1 70 | command: build 71 | flags: 72 | - -trimpath 73 | - -tags osusergo,netgo,sqlite_omit_load_extension 74 | ldflags: 75 | # go tool link -help 76 | # go tool compile -help 77 | # -linkmode external 78 | # -extld=$CC 79 | # -fpic 作为动态链接库的时候 需要添加 80 | 81 | - -s -w -extldflags '-static' -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=dcross 82 | 83 | archives: 84 | - id: "archive1" 85 | format: tar.gz 86 | # this name template makes the OS and Arch compatible with the results of `uname`. 87 | name_template: >- 88 | {{ .ProjectName }}_ 89 | {{- title .Os }}_ 90 | {{- if eq .Arch "amd64" }}x86_64 91 | {{- else if eq .Arch "386" }}i386 92 | {{- else }}{{ .Arch }}{{ end }} 93 | {{- if .Arm }}v{{ .Arm }}{{ end }} 94 | # use zip for windows archives 95 | format_overrides: 96 | - goos: windows 97 | format: zip 98 | 99 | changelog: 100 | sort: asc 101 | filters: 102 | exclude: 103 | - "^docs:" 104 | - "^test:" 105 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #当前目录 4 | cpath=$(pwd) 5 | 6 | ver=$(cat version) 7 | echo $ver 8 | 9 | #前端编译 仅需要执行一次 10 | #bash ./build_web.sh 11 | 12 | bash build_docker.sh 13 | 14 | deploy="anylink-deploy-$ver" 15 | docker container rm $deploy 16 | docker container create --name $deploy bjdgyc/anylink:$ver 17 | rm -rf anylink-deploy anylink-deploy.tar.gz 18 | docker cp -a $deploy:/app ./anylink-deploy 19 | tar zcf ${deploy}.tar.gz anylink-deploy 20 | 21 | 22 | ./anylink-deploy/anylink -v 23 | 24 | 25 | echo "anylink 编译完成,目录: anylink-deploy" 26 | ls -lh anylink-deploy 27 | 28 | 29 | -------------------------------------------------------------------------------- /build_docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | action=$1 4 | 5 | ver=$(cat version) 6 | echo $ver 7 | 8 | # docker login -u bjdgyc 9 | 10 | # 生成时间 2024-01-30T21:41:27+08:00 11 | # date -Iseconds 12 | 13 | #bash ./build_web.sh 14 | 15 | # docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 本地不生成镜像 16 | docker build -t bjdgyc/anylink:latest --no-cache --progress=plain \ 17 | --build-arg CN="yes" --build-arg appVer=$ver --build-arg commitId=$(git rev-parse HEAD) \ 18 | -f docker/Dockerfile . 19 | 20 | echo "docker tag latest $ver" 21 | docker tag bjdgyc/anylink:latest bjdgyc/anylink:$ver 22 | 23 | if [[ $action == "cntest" ]]; then 24 | docker tag bjdgyc/anylink:$ver registry.cn-hangzhou.aliyuncs.com/bjdgyc/anylink:test-$ver 25 | docker push registry.cn-hangzhou.aliyuncs.com/bjdgyc/anylink:test-$ver 26 | echo registry.cn-hangzhou.aliyuncs.com/bjdgyc/anylink:test-$ver 27 | fi 28 | -------------------------------------------------------------------------------- /build_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #github action release.sh 4 | 5 | set -x 6 | function RETVAL() { 7 | rt=$1 8 | if [ $rt != 0 ]; then 9 | echo $rt 10 | exit 1 11 | fi 12 | } 13 | 14 | #当前目录 15 | cpath=$(pwd) 16 | 17 | ver=$(cat version) 18 | echo $ver 19 | 20 | #前端编译 仅需要执行一次 21 | #bash ./build_web.sh 22 | 23 | echo "copy二进制文件" 24 | 25 | # -tags osusergo,netgo,sqlite_omit_load_extension 26 | flags="-trimpath" 27 | ldflags="-s -w -extldflags '-static' -X main.appVer=$ver -X main.commitId=$(git rev-parse HEAD) -X main.buildDate=$(date --iso-8601=seconds)" 28 | #github action 29 | gopath=/go 30 | 31 | dockercmd=$( 32 | cat <> /usr/local/anylink-deploy/log/anylink.log 2>&1' 18 | 19 | # systemd new than v236 20 | # StandardOutput=file:/usr/local/anylink-deploy/log/anylink-systemd.log 21 | # StandardError=file:/usr/local/anylink-deploy/log/anylink-systemd.log 22 | 23 | [Install] 24 | WantedBy=multi-user.target 25 | -------------------------------------------------------------------------------- /deploy/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: anylink 5 | namespace: default 6 | labels: 7 | link-app: anylink 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | link-app: anylink 13 | template: 14 | metadata: 15 | labels: 16 | link-app: anylink 17 | spec: 18 | #hostNetwork: true 19 | dnsPolicy: ClusterFirst 20 | containers: 21 | - name: anylink 22 | env: 23 | - name: NODE_NAME 24 | valueFrom: 25 | fieldRef: 26 | apiVersion: v1 27 | fieldPath: spec.nodeName 28 | - name: GOMAXPROCS 29 | valueFrom: 30 | resourceFieldRef: 31 | resource: limits.cpu 32 | - name: POD_CPU_LIMIT 33 | valueFrom: 34 | resourceFieldRef: 35 | resource: limits.cpu 36 | - name: POD_MEMORY_LIMIT 37 | valueFrom: 38 | resourceFieldRef: 39 | resource: limits.memory 40 | - name: TZ 41 | value: "Asia/Shanghai" 42 | image: bjdgyc/anylink:latest 43 | imagePullPolicy: Always 44 | args: 45 | - --conf=/app/conf/server.toml 46 | ports: 47 | - name: https 48 | containerPort: 443 49 | protocol: TCP 50 | - name: https-admin 51 | containerPort: 8800 52 | protocol: TCP 53 | - name: dtls 54 | containerPort: 443 55 | protocol: UDP 56 | # 设置资源 57 | resources: 58 | limits: 59 | cpu: "2" 60 | memory: 4Gi 61 | ephemeral-storage: "2Gi" 62 | securityContext: 63 | privileged: true 64 | # 禁用自动注入 service 信息到环境变量 65 | enableServiceLinks: false 66 | restartPolicy: Always 67 | terminationGracePeriodSeconds: 30 68 | nodeSelector: 69 | kubernetes.io/os: linux 70 | securityContext: { } 71 | tolerations: 72 | - operator: Exists 73 | #设置优先级 74 | priorityClassName: system-cluster-critical 75 | 76 | --- 77 | apiVersion: v1 78 | kind: Service 79 | metadata: 80 | name: anylink 81 | namespace: default 82 | labels: 83 | link-app: anylink 84 | spec: 85 | ports: 86 | - name: https 87 | port: 443 88 | targetPort: 443 89 | protocol: TCP 90 | - name: https-admin 91 | port: 8800 92 | targetPort: 8800 93 | protocol: TCP 94 | - name: dtls 95 | port: 443 96 | targetPort: 443 97 | protocol: UDP 98 | selector: 99 | link-app: anylink 100 | sessionAffinity: ClientIP 101 | type: ClusterIP 102 | -------------------------------------------------------------------------------- /deploy/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | anylink: 3 | image: bjdgyc/anylink:latest 4 | container_name: anylink 5 | restart: always 6 | privileged: true 7 | #cpus: 2 8 | #mem_limit: 4g 9 | ports: 10 | - 443:443 11 | - 8800:8800 12 | - 443:443/udp 13 | environment: 14 | LINK_LOG_LEVEL: info 15 | #IPTABLES_LEGACY: "on" 16 | command: 17 | - --conf=/app/conf/server.toml 18 | #volumes: 19 | # - /home/myconf:/app/conf 20 | dns_search: . 21 | -------------------------------------------------------------------------------- /deploy_docker_cn.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ver=$(cat version) 4 | echo $ver 5 | 6 | echo "docker tag latest $ver" 7 | 8 | docker pull --platform=linux/amd64 bjdgyc/anylink:$ver 9 | 10 | docker tag bjdgyc/anylink:$ver registry.cn-hangzhou.aliyuncs.com/bjdgyc/anylink:latest 11 | docker push registry.cn-hangzhou.aliyuncs.com/bjdgyc/anylink:latest 12 | 13 | docker tag bjdgyc/anylink:$ver registry.cn-hangzhou.aliyuncs.com/bjdgyc/anylink:$ver 14 | docker push registry.cn-hangzhou.aliyuncs.com/bjdgyc/anylink:$ver 15 | 16 | docker rmi bjdgyc/anylink:$ver 17 | 18 | #arm64 19 | docker pull --platform=linux/arm64 bjdgyc/anylink:$ver 20 | 21 | docker tag bjdgyc/anylink:$ver registry.cn-hangzhou.aliyuncs.com/bjdgyc/anylink:arm64v8-latest 22 | docker push registry.cn-hangzhou.aliyuncs.com/bjdgyc/anylink:arm64v8-latest 23 | 24 | docker tag bjdgyc/anylink:$ver registry.cn-hangzhou.aliyuncs.com/bjdgyc/anylink:arm64v8-$ver 25 | docker push registry.cn-hangzhou.aliyuncs.com/bjdgyc/anylink:arm64v8-$ver 26 | 27 | docker rmi bjdgyc/anylink:$ver 28 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | ## Donate 2 | 3 | > 如果您觉得 AnyLink 对你有帮助,欢迎给我们打赏,也是帮助 AnyLink 更好的发展。 4 | 5 |

6 | anylink捐赠二维码 7 |

8 | 9 | ## Donator 10 | 11 | > 感谢以下同学的打赏,AnyLink 有你更美好! 12 | > 13 | > 需要展示主页的同学,可以在QQ群 直接联系我添加。 14 | 15 | | 昵称 | 主页 / 联系方式 | 16 | |--------------------|------------------------------| 17 | | 代码 oo8 | | 18 | | 甘磊 | https://github.com/ganlei333 | 19 | | Oo@ | https://github.com/chooop | 20 | | 虚极静笃 | | 21 | | 请喝可乐 | | 22 | | 加油加油 | | 23 | | 李建 | | 24 | | lanbin | | 25 | | 乐在东途 | | 26 | | 孤鸿 | | 27 | | 刘国华 | | 28 | | 改名好无聊 | | 29 | | 全能互联网专家 | | 30 | | JCM | | 31 | | Eh... | | 32 | | 沉 | | 33 | | 刘国华 | | 34 | | 忧郁的豚骨拉面 | | 35 | | 张小旋当爹地 | | 36 | | 对方正在输入 | | 37 | | Ronny | | 38 | | 奔跑的少年 | | 39 | | ZBW | | 40 | | 悲鸣 | | 41 | | 谢谢 | | 42 | | 云思科技 | | 43 | | 哆啦A伟(张佳伟) | | 44 | | 人类的悲欢并不相通 | | 45 | | 做人要低调 | | 46 | | 洛洛 | | 47 | | Dragon Liao | | 48 | | 诸葛御风 | | 49 | | 杨杨得亿 | | 50 | | Thanataos | | 51 | | 憨大叔 | | 52 | | 明月 | | 53 | | Amis | | 54 | | Blake | | 55 | | 刘国华 | | 56 | | ZBW | | 57 | | 全能互联网专家 | | 58 | | 广播.会议.音响.无纸化.物联网中控 | | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /doc/question.md: -------------------------------------------------------------------------------- 1 | ## 常见问题 2 | 3 | ### anyconnect 客户端问题 4 | 5 | > 客户端请使用群共享文件的版本,其他版本没有测试过,不保证使用正常 6 | > 7 | > 添加QQ群: 567510628 8 | 9 | ### OTP 动态码 10 | 11 | > 请使用手机安装 freeotp ,然后扫描otp二维码,生成的数字即是动态码 12 | 13 | ### 用户策略问题 14 | 15 | > 只要有用户策略,组策略就不生效,相当于覆盖了组策略的配置 16 | 17 | ### 远程桌面连接 18 | 19 | > 本软件已经支持远程桌面里面连接anyconnect。 20 | 21 | ### 私有证书问题 22 | 23 | > anylink 默认不支持私有证书 24 | > 25 | > 其他使用私有证书的问题,请自行解决 26 | 27 | ### 客户端连接名称 28 | 29 | > 客户端连接名称需要修改 [profile.xml](../server/conf/profile.xml) 文件 30 | 31 | ```xml 32 | 33 | 34 | VPN 35 | localhost 36 | 37 | ``` 38 | 39 | ### dpd timeout 设置问题 40 | 41 | ```yaml 42 | #客户端失效检测时间(秒) dpd > keepalive 43 | cstp_keepalive = 4 44 | cstp_dpd = 9 45 | mobile_keepalive = 7 46 | mobile_dpd = 15 47 | ``` 48 | 49 | > 以上dpd参数为客户端的超时检测时间, 如一段时间内,没有数据传输,防火墙会主动关闭连接 50 | > 51 | > 如经常出现 timeout 的错误信息,应根据当前防火墙的设置,适当减小dpd数值 52 | 53 | ### 关于审计日志 audit_interval 参数 54 | 55 | > 默认值 `audit_interval = 600` 表示相同日志600秒内只记录一次,不同日志首次出现立即记录 56 | > 57 | > 去重key的格式: 16字节源IP地址 + 16字节目的IP地址 + 2字节目的端口 + 1字节协议类型 + 16字节域名MD5 58 | 59 | ### 反向代理问题 60 | 61 | > anylink 仅支持四层反向代理,不支持七层反向代理 62 | > 63 | > 如Nginx请使用 stream模块 64 | 65 | ```conf 66 | stream { 67 | upstream anylink_server { 68 | server 127.0.0.1:8443; 69 | } 70 | server { 71 | listen 443 tcp; 72 | proxy_timeout 30s; 73 | proxy_pass anylink_server; 74 | } 75 | } 76 | ``` 77 | 78 | > nginx实现 共用443端口 示例 79 | 80 | ```conf 81 | stream { 82 | map $ssl_preread_server_name $name { 83 | vpn.xx.com myvpn; 84 | default defaultpage; 85 | } 86 | 87 | # upstream pool 88 | upstream myvpn { 89 | server 127.0.0.1:8443; 90 | } 91 | upstream defaultpage { 92 | server 127.0.0.1:8080; 93 | } 94 | 95 | server { 96 | listen 443 so_keepalive=on; 97 | ssl_preread on; 98 | #接收端也需要设置 proxy_protocol 99 | #proxy_protocol on; 100 | proxy_pass $name; 101 | } 102 | } 103 | 104 | ``` 105 | 106 | ### 性能问题 107 | 108 | ``` 109 | 内网环境测试数据 110 | 虚拟服务器: centos7 4C8G 111 | anylink: tun模式 tcp传输 112 | 客户端文件下载速度:240Mb/s 113 | 客户端网卡下载速度:270Mb/s 114 | 服务端网卡上传速度:280Mb/s 115 | ``` 116 | 117 | > 客户端tls加密协议、隧道header头都会占用一定带宽 118 | 119 | 120 | ### 登录防爆说明 121 | 122 | ``` 123 | 1.用户 A 在 IP 1.2.3.4 上尝试登录: 124 | 用户 A 在 IP 1.2.3.4 上尝试登录失败 5 次,触发了该 IP 上的用户 A 锁定 5 分钟。 125 | 在这 5 分钟内,用户 A 从 IP 1.2.3.4 无法进行新的登录尝试。 126 | 2.用户 A 更换 IP 到 1.2.3.5 继续尝试登录: 127 | 用户 A 在 IP 1.2.3.5 上继续尝试登录,并且累计失败 20 次,触发了全局用户 A 锁定 5 分钟。 128 | 在这 5 分钟内,用户 A 从任何 IP 地址都无法进行新的登录尝试。 129 | 3.IP 1.2.3.4 上多个用户尝试登录: 130 | 如果从 IP 1.2.3.4 上累计有 40 次失败登录尝试(无论来自多少不同的用户),触发了该 IP 的全局锁定 5 分钟。 131 | 在这 5 分钟内,从 IP 1.2.3.4 的所有登录尝试都将被拒绝。 132 | 133 | 如果在 N 分钟内没有新的失败尝试,失败计数会在 N 分钟后(*_reset_time)重置。 134 | ``` -------------------------------------------------------------------------------- /doc/screenshot/group.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjdgyc/anylink/805498e270a2172f116161f3534cfcf76de2f83d/doc/screenshot/group.jpg -------------------------------------------------------------------------------- /doc/screenshot/ip_map.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjdgyc/anylink/805498e270a2172f116161f3534cfcf76de2f83d/doc/screenshot/ip_map.jpg -------------------------------------------------------------------------------- /doc/screenshot/jetbrains.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjdgyc/anylink/805498e270a2172f116161f3534cfcf76de2f83d/doc/screenshot/jetbrains.png -------------------------------------------------------------------------------- /doc/screenshot/online.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjdgyc/anylink/805498e270a2172f116161f3534cfcf76de2f83d/doc/screenshot/online.jpg -------------------------------------------------------------------------------- /doc/screenshot/qq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjdgyc/anylink/805498e270a2172f116161f3534cfcf76de2f83d/doc/screenshot/qq.png -------------------------------------------------------------------------------- /doc/screenshot/qq2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjdgyc/anylink/805498e270a2172f116161f3534cfcf76de2f83d/doc/screenshot/qq2.jpg -------------------------------------------------------------------------------- /doc/screenshot/setting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjdgyc/anylink/805498e270a2172f116161f3534cfcf76de2f83d/doc/screenshot/setting.jpg -------------------------------------------------------------------------------- /doc/screenshot/system.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjdgyc/anylink/805498e270a2172f116161f3534cfcf76de2f83d/doc/screenshot/system.jpg -------------------------------------------------------------------------------- /doc/screenshot/users.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjdgyc/anylink/805498e270a2172f116161f3534cfcf76de2f83d/doc/screenshot/users.jpg -------------------------------------------------------------------------------- /doc/screenshot/wxpay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjdgyc/anylink/805498e270a2172f116161f3534cfcf76de2f83d/doc/screenshot/wxpay.png -------------------------------------------------------------------------------- /doc/screenshot/wxpay2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjdgyc/anylink/805498e270a2172f116161f3534cfcf76de2f83d/doc/screenshot/wxpay2.png -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | #node:16-bullseye 2 | #golang:1.20-bullseye 3 | #debian:bullseye-slim 4 | #bullseye 5 | # sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list 6 | #bookworm 7 | # sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources 8 | 9 | # sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories 10 | 11 | 12 | # 配合 github action 使用 13 | # 需要先编译出ui文件后 再执行docker编译 14 | 15 | # server 16 | # golang:1.20-alpine3.19 17 | FROM golang:1.22-alpine3.19 as builder_golang 18 | 19 | ARG CN="no" 20 | ARG appVer="appVer" 21 | ARG commitId="commitId" 22 | 23 | ENV TZ=Asia/Shanghai 24 | 25 | WORKDIR /server 26 | COPY docker/init_build.sh /tmp/ 27 | COPY server/ /server/ 28 | COPY web/ui /server/ui 29 | 30 | #RUN apk add gcc musl-dev bash 31 | RUN sh /tmp/init_build.sh 32 | 33 | 34 | # anylink 35 | FROM alpine:3.19 36 | LABEL maintainer="github.com/bjdgyc" 37 | 38 | ARG CN="no" 39 | 40 | ENV TZ=Asia/Shanghai 41 | #开关变量 on off 42 | ENV ANYLINK_IN_CONTAINER="on" 43 | ENV IPTABLES_LEGACY="off" 44 | 45 | WORKDIR /app 46 | COPY docker/init_release.sh /tmp/ 47 | 48 | COPY --from=builder_golang /server/anylink /app/ 49 | COPY docker/docker_entrypoint.sh server/bridge-init.sh ./README.md ./LICENSE version_info /app/ 50 | COPY ./deploy /app/deploy 51 | COPY ./index_template /app/index_template 52 | COPY ./server/conf /app/conf 53 | 54 | #TODO 本地打包时使用镜像 55 | RUN sh /tmp/init_release.sh 56 | 57 | 58 | EXPOSE 443 8800 443/udp 59 | 60 | #CMD ["/app/anylink"] 61 | ENTRYPOINT ["/app/docker_entrypoint.sh"] 62 | 63 | -------------------------------------------------------------------------------- /docker/docker_entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | var1=$1 3 | 4 | #set -x 5 | 6 | case $var1 in 7 | "bash" | "sh") 8 | echo $var1 9 | exec "$@" 10 | ;; 11 | 12 | "tool") 13 | /app/anylink "$@" 14 | ;; 15 | 16 | *) 17 | #sysctl -w net.ipv4.ip_forward=1 18 | #iptables -t nat -A POSTROUTING -s "${IPV4_CIDR}" -o eth0+ -j MASQUERADE 19 | #iptables -nL -t nat 20 | 21 | # 启动服务 先判断配置文件是否存在 22 | if [[ ! -f /app/conf/profile.xml ]]; then 23 | /bin/cp -r /home/conf-bak/* /app/conf/ 24 | echo "After the configuration file is initialized, the container will be forcibly exited. Restart the container." 25 | echo "配置文件初始化完成后,容器会强制退出,请重新启动容器。" 26 | exit 1 27 | fi 28 | 29 | # 兼容老版本 iptables 30 | if [[ $IPTABLES_LEGACY == "on" ]]; then 31 | rm /sbin/iptables 32 | ln -s /sbin/iptables-legacy /sbin/iptables 33 | fi 34 | 35 | exec /app/anylink "$@" 36 | ;; 37 | esac 38 | -------------------------------------------------------------------------------- /docker/generate-certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p /ssl 4 | 5 | OUTPUT_FILENAME="vpn.xx.com" 6 | 7 | printf "[req] 8 | prompt = no 9 | default_bits = 4096 10 | default_md = sha256 11 | encrypt_key = no 12 | string_mask = utf8only 13 | 14 | distinguished_name = cert_distinguished_name 15 | req_extensions = req_x509v3_extensions 16 | x509_extensions = req_x509v3_extensions 17 | 18 | [ cert_distinguished_name ] 19 | C = CN 20 | ST = BJ 21 | L = BJ 22 | O = xx.com 23 | OU = xx.com 24 | CN = xx.com 25 | 26 | [req_x509v3_extensions] 27 | basicConstraints = critical,CA:true 28 | subjectKeyIdentifier = hash 29 | keyUsage = critical,digitalSignature,keyCertSign,cRLSign #,keyEncipherment 30 | extendedKeyUsage = critical,serverAuth #, clientAuth 31 | subjectAltName = @alt_names 32 | 33 | [alt_names] 34 | DNS.1 = xx.com 35 | DNS.2 = *.xx.com 36 | 37 | ">/ssl/${OUTPUT_FILENAME}.conf 38 | 39 | openssl req -x509 -newkey rsa:2048 -keyout /ssl/test_vpn_key.pem -out /ssl/test_vpn_cert.pem \ 40 | -days 3600 -nodes -config /ssl/${OUTPUT_FILENAME}.conf 41 | 42 | -------------------------------------------------------------------------------- /docker/init_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -x 4 | 5 | #TODO 本地打包时使用镜像 6 | if [[ $CN == "yes" ]]; then 7 | #sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories 8 | sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories 9 | export GOPROXY=https://goproxy.cn 10 | fi 11 | 12 | apk add build-base tzdata gcc g++ musl musl-dev upx 13 | 14 | uname -a 15 | env 16 | date 17 | 18 | cd /server 19 | 20 | go mod tidy 21 | 22 | echo "start build" 23 | 24 | ldflags="-s -w -X main.appVer=$appVer -X main.commitId=$commitId -X main.buildDate=$(date -Iseconds) -extldflags \"-static\" " 25 | 26 | export CGO_ENABLED=1 27 | go build -v -o anylink -trimpath -ldflags "$ldflags" 28 | 29 | ls -lh /server/ 30 | 31 | # 压缩文件 32 | upx -9 -k anylink 33 | 34 | /server/anylink -v 35 | -------------------------------------------------------------------------------- /docker/init_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -x 4 | 5 | #TODO 本地打包时使用镜像 6 | if [[ $CN == "yes" ]]; then 7 | #sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories 8 | sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories 9 | export GOPROXY=https://goproxy.cn 10 | fi 11 | 12 | 13 | # docker 启动使用 4.19 以上内核 14 | apk add --no-cache ca-certificates bash iproute2 tzdata iptables inetutils-telnet 15 | 16 | # alpine:3.19 兼容老版本 iptables 17 | apk add --no-cache iptables-legacy 18 | 19 | #rm /sbin/iptables 20 | #ln -s /sbin/iptables-legacy /sbin/iptables 21 | 22 | 23 | chmod +x /app/docker_entrypoint.sh 24 | mkdir /app/log 25 | 26 | #备份配置文件 27 | cp -r /app/conf /home/conf-bak 28 | 29 | tree /app 30 | 31 | uname -a 32 | date -Iseconds 33 | -------------------------------------------------------------------------------- /index_template/自定义首页1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AnyLink - 企业级远程办公 SSL VPN 6 | 63 | 64 | 65 |
66 |

欢迎使用 AnyLink

67 |
68 | 69 |
70 |

什么是 AnyLink?

71 |

AnyLink 是一款面向企业级的远程办公 SSL VPN 软件,支持多人同时在线使用。它提供安全、便捷的访问内部网络资源的方式,使远程工作者能够有效协作。

72 | 73 |

核心功能

74 | 80 | 81 |

开始使用 AnyLink

82 |

体验 AnyLink 为您的企业远程办公需求所带来的便利和安全。

83 | 84 |

下载客户端

85 | Windows 客户端 86 | Mac 客户端 87 | 88 | iOS 客户端 89 | Android 客户端 90 | 91 | Android FreeOTP客户端 92 | iOS FreeOTP客户端 93 |

使用手册

94 | 使用手册(Windows) 95 |
96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #github action release.sh 4 | 5 | set -x 6 | function RETVAL() { 7 | rt=$1 8 | if [ $rt != 0 ]; then 9 | echo $rt 10 | exit 1 11 | fi 12 | } 13 | 14 | #当前目录 15 | cpath=$(pwd) 16 | 17 | ver=$(cat version) 18 | echo "当前版本 $ver" 19 | 20 | rm -rf artifact-dist 21 | mkdir artifact-dist 22 | 23 | function archive() { 24 | arch=$1 25 | #echo "整理部署文件 $arch" 26 | arch_name=${arch//\//-} 27 | echo $arch_name 28 | 29 | deploy="anylink-$ver-$arch_name" 30 | docker container rm $deploy 31 | docker container create --platform $arch --name $deploy bjdgyc/anylink:$ver 32 | rm -rf anylink-deploy 33 | docker cp -a $deploy:/app ./anylink-deploy 34 | 35 | ls -lh anylink-deploy 36 | 37 | tar zcf ${deploy}.tar.gz anylink-deploy 38 | mv ${deploy}.tar.gz artifact-dist/ 39 | } 40 | 41 | echo "copy二进制文件" 42 | 43 | archive "linux/amd64" 44 | archive "linux/arm64" 45 | 46 | ls -lh artifact-dist 47 | 48 | #注意使用root权限运行 49 | #cd anylink-deploy 50 | #sudo ./anylink --conf="conf/server.toml" 51 | -------------------------------------------------------------------------------- /server/.codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "screenshot" 3 | - "web" 4 | - "server/conf" 5 | - "server/files" -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | vendor/ 16 | 17 | ui/ 18 | .idea/ 19 | anylink 20 | data.db 21 | conf/*.db 22 | -------------------------------------------------------------------------------- /server/admin/api_base.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "runtime/debug" 7 | "time" 8 | 9 | "github.com/bjdgyc/anylink/base" 10 | "github.com/bjdgyc/anylink/pkg/utils" 11 | "github.com/gorilla/mux" 12 | "github.com/xlzd/gotp" 13 | ) 14 | 15 | // Login 登陆接口 16 | func Login(w http.ResponseWriter, r *http.Request) { 17 | // TODO 调试信息输出 18 | // hd, _ := httputil.DumpRequest(r, true) 19 | // fmt.Println("DumpRequest: ", string(hd)) 20 | 21 | _ = r.ParseForm() 22 | adminUser := r.PostFormValue("admin_user") 23 | adminPass := r.PostFormValue("admin_pass") 24 | 25 | // 启用otp验证 26 | if base.Cfg.AdminOtp != "" { 27 | pwd := adminPass 28 | pl := len(pwd) 29 | if pl < 6 { 30 | RespError(w, RespUserOrPassErr) 31 | base.Error(adminUser, "管理员otp错误") 32 | return 33 | } 34 | // 判断otp信息 35 | adminPass = pwd[:pl-6] 36 | otp := pwd[pl-6:] 37 | 38 | totp := gotp.NewDefaultTOTP(base.Cfg.AdminOtp) 39 | unix := time.Now().Unix() 40 | verify := totp.Verify(otp, unix) 41 | 42 | if !verify { 43 | RespError(w, RespUserOrPassErr) 44 | base.Error(adminUser, "管理员otp错误") 45 | return 46 | } 47 | } 48 | 49 | // 认证错误 50 | if !(adminUser == base.Cfg.AdminUser && 51 | utils.PasswordVerify(adminPass, base.Cfg.AdminPass)) { 52 | RespError(w, RespUserOrPassErr) 53 | base.Error(adminUser, "管理员用户名或密码错误") 54 | return 55 | } 56 | 57 | // token有效期 58 | expiresAt := time.Now().Unix() + 3600*3 59 | jwtData := map[string]interface{}{"admin_user": adminUser} 60 | tokenString, err := SetJwtData(jwtData, expiresAt) 61 | if err != nil { 62 | RespError(w, 1, err) 63 | return 64 | } 65 | 66 | data := make(map[string]interface{}) 67 | data["token"] = tokenString 68 | data["admin_user"] = adminUser 69 | data["expires_at"] = expiresAt 70 | 71 | ck := &http.Cookie{ 72 | Name: "jwt", 73 | Value: tokenString, 74 | Path: "/", 75 | HttpOnly: true, 76 | } 77 | http.SetCookie(w, ck) 78 | 79 | RespSucess(w, data) 80 | } 81 | 82 | func authMiddleware(next http.Handler) http.Handler { 83 | fn := func(w http.ResponseWriter, r *http.Request) { 84 | w.Header().Set("Access-Control-Allow-Origin", "*") 85 | w.Header().Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS") 86 | w.Header().Set("Access-Control-Allow-Headers", "*") 87 | if r.Method == http.MethodOptions { 88 | // w.WriteHeader(http.StatusOK) 89 | // 正式环境不支持 OPTIONS 90 | w.WriteHeader(http.StatusForbidden) 91 | return 92 | } 93 | 94 | route := mux.CurrentRoute(r) 95 | name := route.GetName() 96 | // fmt.Println("bb", r.URL.Path, name) 97 | if utils.InArrStr([]string{"login", "index", "static"}, name) { 98 | // 不进行鉴权 99 | next.ServeHTTP(w, r) 100 | return 101 | } 102 | 103 | // 进行登陆鉴权 104 | jwtToken := r.Header.Get("Jwt") 105 | if jwtToken == "" { 106 | jwtToken = r.FormValue("jwt") 107 | } 108 | if jwtToken == "" { 109 | cc, err := r.Cookie("jwt") 110 | if err == nil { 111 | jwtToken = cc.Value 112 | } 113 | } 114 | data, err := GetJwtData(jwtToken) 115 | if err != nil || base.Cfg.AdminUser != fmt.Sprint(data["admin_user"]) { 116 | w.WriteHeader(http.StatusUnauthorized) 117 | return 118 | } 119 | next.ServeHTTP(w, r) 120 | } 121 | 122 | return http.HandlerFunc(fn) 123 | } 124 | 125 | func recoverHttp(next http.Handler) http.Handler { 126 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 127 | defer func() { 128 | if err := recover(); err != nil { 129 | stack := debug.Stack() 130 | base.Error(err, string(stack)) 131 | // http.Error(w, "Internal Server Error", 500) 132 | RespError(w, 500, "Internal Server Error") 133 | } 134 | }() 135 | next.ServeHTTP(w, r) 136 | }) 137 | } 138 | -------------------------------------------------------------------------------- /server/admin/api_cert.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/bjdgyc/anylink/base" 11 | "github.com/bjdgyc/anylink/dbdata" 12 | ) 13 | 14 | func CustomCert(w http.ResponseWriter, r *http.Request) { 15 | cert, _, err := r.FormFile("cert") 16 | if err != nil { 17 | RespError(w, RespInternalErr, err) 18 | return 19 | } 20 | key, _, err := r.FormFile("key") 21 | if err != nil { 22 | RespError(w, RespInternalErr, err) 23 | return 24 | } 25 | certFile, err := os.OpenFile(base.Cfg.CertFile, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600) 26 | if err != nil { 27 | RespError(w, RespInternalErr, err) 28 | return 29 | } 30 | defer certFile.Close() 31 | if _, err := io.Copy(certFile, cert); err != nil { 32 | RespError(w, RespInternalErr, err) 33 | return 34 | } 35 | keyFile, err := os.OpenFile(base.Cfg.CertKey, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600) 36 | if err != nil { 37 | RespError(w, RespInternalErr, err) 38 | return 39 | } 40 | defer keyFile.Close() 41 | if _, err := io.Copy(keyFile, key); err != nil { 42 | RespError(w, RespInternalErr, err) 43 | return 44 | } 45 | if tlscert, _, err := dbdata.ParseCert(); err != nil { 46 | RespError(w, RespInternalErr, fmt.Sprintf("证书不合法,请重新上传:%v", err)) 47 | return 48 | } else { 49 | dbdata.LoadCertificate(tlscert) 50 | } 51 | RespSucess(w, "上传成功") 52 | } 53 | func GetCertSetting(w http.ResponseWriter, r *http.Request) { 54 | sess := dbdata.GetXdb().NewSession() 55 | defer sess.Close() 56 | data := &dbdata.SettingLetsEncrypt{} 57 | if err := dbdata.SettingGet(data); err != nil { 58 | dbdata.SettingSessAdd(sess, data) 59 | RespError(w, RespInternalErr, err) 60 | } 61 | userData := &dbdata.LegoUserData{} 62 | if err := dbdata.SettingGet(userData); err != nil { 63 | dbdata.SettingSessAdd(sess, userData) 64 | } 65 | RespSucess(w, data) 66 | } 67 | func CreatCert(w http.ResponseWriter, r *http.Request) { 68 | if err := r.ParseForm(); err != nil { 69 | http.Error(w, err.Error(), http.StatusBadRequest) 70 | return 71 | } 72 | body, err := io.ReadAll(r.Body) 73 | if err != nil { 74 | RespError(w, RespInternalErr, err) 75 | return 76 | } 77 | defer r.Body.Close() 78 | config := &dbdata.SettingLetsEncrypt{} 79 | if err := json.Unmarshal(body, config); err != nil { 80 | RespError(w, RespInternalErr, err) 81 | return 82 | } 83 | if err := dbdata.SettingSet(config); err != nil { 84 | RespError(w, RespInternalErr, err) 85 | return 86 | } 87 | client := dbdata.LeGoClient{} 88 | if err := client.NewClient(config); err != nil { 89 | base.Error(err) 90 | RespError(w, RespInternalErr, fmt.Sprintf("获取证书失败:%v", err)) 91 | return 92 | } 93 | if err := client.GetCert(config.Domain); err != nil { 94 | base.Error(err) 95 | RespError(w, RespInternalErr, fmt.Sprintf("获取证书失败:%v", err)) 96 | return 97 | } 98 | RespSucess(w, "生成证书成功") 99 | } 100 | -------------------------------------------------------------------------------- /server/admin/api_group.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/bjdgyc/anylink/dbdata" 10 | ) 11 | 12 | func GroupList(w http.ResponseWriter, r *http.Request) { 13 | _ = r.ParseForm() 14 | pageS := r.FormValue("page") 15 | page, _ := strconv.Atoi(pageS) 16 | if page < 1 { 17 | page = 1 18 | } 19 | 20 | var pageSize = dbdata.PageSize 21 | 22 | count := dbdata.CountAll(&dbdata.Group{}) 23 | 24 | var datas []dbdata.Group 25 | err := dbdata.Find(&datas, pageSize, page) 26 | if err != nil { 27 | RespError(w, RespInternalErr, err) 28 | return 29 | } 30 | 31 | data := map[string]interface{}{ 32 | "count": count, 33 | "page_size": pageSize, 34 | "datas": datas, 35 | } 36 | 37 | RespSucess(w, data) 38 | } 39 | 40 | func GroupNames(w http.ResponseWriter, r *http.Request) { 41 | var names = dbdata.GetGroupNames() 42 | data := map[string]interface{}{ 43 | "count": len(names), 44 | "page_size": 0, 45 | "datas": names, 46 | } 47 | RespSucess(w, data) 48 | } 49 | 50 | func GroupNamesIds(w http.ResponseWriter, r *http.Request) { 51 | var names = dbdata.GetGroupNamesIds() 52 | data := map[string]interface{}{ 53 | "count": len(names), 54 | "page_size": 0, 55 | "datas": names, 56 | } 57 | RespSucess(w, data) 58 | } 59 | 60 | func GroupDetail(w http.ResponseWriter, r *http.Request) { 61 | _ = r.ParseForm() 62 | idS := r.FormValue("id") 63 | id, _ := strconv.Atoi(idS) 64 | if id < 1 { 65 | RespError(w, RespParamErr, "Id错误") 66 | return 67 | } 68 | 69 | var data dbdata.Group 70 | err := dbdata.One("Id", id, &data) 71 | if err != nil { 72 | RespError(w, RespInternalErr, err) 73 | return 74 | } 75 | if len(data.Auth) == 0 { 76 | data.Auth["type"] = "local" 77 | } 78 | // 兼容旧数据 79 | if data.SplitDns == nil { 80 | data.SplitDns = []dbdata.ValData{} 81 | } 82 | RespSucess(w, data) 83 | } 84 | 85 | func GroupSet(w http.ResponseWriter, r *http.Request) { 86 | body, err := io.ReadAll(r.Body) 87 | if err != nil { 88 | RespError(w, RespInternalErr, err) 89 | return 90 | } 91 | defer r.Body.Close() 92 | v := &dbdata.Group{} 93 | err = json.Unmarshal(body, v) 94 | if err != nil { 95 | RespError(w, RespInternalErr, err) 96 | return 97 | } 98 | 99 | err = dbdata.SetGroup(v) 100 | if err != nil { 101 | RespError(w, RespInternalErr, err) 102 | return 103 | } 104 | 105 | RespSucess(w, nil) 106 | } 107 | 108 | func GroupDel(w http.ResponseWriter, r *http.Request) { 109 | _ = r.ParseForm() 110 | idS := r.FormValue("id") 111 | id, _ := strconv.Atoi(idS) 112 | if id < 1 { 113 | RespError(w, RespParamErr, "Id错误") 114 | return 115 | } 116 | 117 | data := dbdata.Group{Id: id} 118 | err := dbdata.Del(&data) 119 | if err != nil { 120 | RespError(w, RespInternalErr, err) 121 | return 122 | } 123 | RespSucess(w, nil) 124 | } 125 | 126 | func GroupAuthLogin(w http.ResponseWriter, r *http.Request) { 127 | type AuthLoginData struct { 128 | Name string `json:"name"` 129 | Pwd string `json:"pwd"` 130 | Auth map[string]interface{} `json:"auth"` 131 | } 132 | 133 | body, err := io.ReadAll(r.Body) 134 | if err != nil { 135 | RespError(w, RespInternalErr, err) 136 | return 137 | } 138 | defer r.Body.Close() 139 | v := &AuthLoginData{} 140 | err = json.Unmarshal(body, &v) 141 | if err != nil { 142 | RespError(w, RespInternalErr, err) 143 | return 144 | } 145 | err = dbdata.GroupAuthLogin(v.Name, v.Pwd, v.Auth) 146 | if err != nil { 147 | RespError(w, RespInternalErr, err) 148 | return 149 | } 150 | RespSucess(w, "ok") 151 | } 152 | -------------------------------------------------------------------------------- /server/admin/api_ip_map.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/bjdgyc/anylink/dbdata" 10 | ) 11 | 12 | func UserIpMapList(w http.ResponseWriter, r *http.Request) { 13 | _ = r.ParseForm() 14 | pageS := r.FormValue("page") 15 | page, _ := strconv.Atoi(pageS) 16 | if page < 1 { 17 | page = 1 18 | } 19 | 20 | var pageSize = dbdata.PageSize 21 | 22 | count := dbdata.CountAll(&dbdata.IpMap{}) 23 | 24 | var datas []dbdata.IpMap 25 | err := dbdata.Find(&datas, pageSize, page) 26 | if err != nil { 27 | RespError(w, RespInternalErr, err) 28 | return 29 | } 30 | 31 | data := map[string]interface{}{ 32 | "count": count, 33 | "page_size": pageSize, 34 | "datas": datas, 35 | } 36 | 37 | RespSucess(w, data) 38 | } 39 | 40 | func UserIpMapDetail(w http.ResponseWriter, r *http.Request) { 41 | _ = r.ParseForm() 42 | idS := r.FormValue("id") 43 | id, _ := strconv.Atoi(idS) 44 | if id < 1 { 45 | RespError(w, RespParamErr, "用户名错误") 46 | return 47 | } 48 | 49 | var data dbdata.IpMap 50 | err := dbdata.One("Id", id, &data) 51 | if err != nil { 52 | RespError(w, RespInternalErr, err) 53 | return 54 | } 55 | 56 | RespSucess(w, data) 57 | } 58 | 59 | func UserIpMapSet(w http.ResponseWriter, r *http.Request) { 60 | _ = r.ParseForm() 61 | 62 | body, err := io.ReadAll(r.Body) 63 | if err != nil { 64 | RespError(w, RespInternalErr, err) 65 | return 66 | } 67 | defer r.Body.Close() 68 | v := &dbdata.IpMap{} 69 | err = json.Unmarshal(body, v) 70 | if err != nil { 71 | RespError(w, RespInternalErr, err) 72 | return 73 | } 74 | 75 | // fmt.Println(v, len(v.Ip), len(v.MacAddr)) 76 | 77 | err = dbdata.SetIpMap(v) 78 | if err != nil { 79 | RespError(w, RespInternalErr, err) 80 | return 81 | } 82 | 83 | // sessdata.IpAllSet(v) 84 | 85 | RespSucess(w, nil) 86 | } 87 | 88 | func UserIpMapDel(w http.ResponseWriter, r *http.Request) { 89 | _ = r.ParseForm() 90 | idS := r.FormValue("id") 91 | id, _ := strconv.Atoi(idS) 92 | 93 | if id < 1 { 94 | RespError(w, RespParamErr, "IP映射id错误") 95 | return 96 | } 97 | 98 | var data dbdata.IpMap 99 | err := dbdata.One("Id", id, &data) 100 | if err != nil { 101 | RespError(w, RespInternalErr, err) 102 | return 103 | } 104 | 105 | err = dbdata.Del(&data) 106 | if err != nil { 107 | RespError(w, RespInternalErr, err) 108 | return 109 | } 110 | 111 | // sessdata.IpAllDel(&data) 112 | 113 | RespSucess(w, nil) 114 | } 115 | -------------------------------------------------------------------------------- /server/admin/api_other.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "net/http" 8 | "regexp" 9 | 10 | "github.com/bjdgyc/anylink/base" 11 | "github.com/bjdgyc/anylink/dbdata" 12 | ) 13 | 14 | func setOtherGet(data interface{}, w http.ResponseWriter) { 15 | err := dbdata.SettingGet(data) 16 | if err != nil && !dbdata.CheckErrNotFound(err) { 17 | RespError(w, RespInternalErr, err) 18 | return 19 | } 20 | // 不明文输出SMTP的密码 21 | switch dbdata.StructName(data) { 22 | case "SettingSmtp": 23 | data.(*dbdata.SettingSmtp).Password = "" 24 | } 25 | RespSucess(w, data) 26 | } 27 | 28 | func setOtherEdit(data interface{}, w http.ResponseWriter, r *http.Request) { 29 | body, err := io.ReadAll(r.Body) 30 | if err != nil { 31 | RespError(w, RespInternalErr, err) 32 | return 33 | } 34 | defer r.Body.Close() 35 | 36 | err = json.Unmarshal(body, data) 37 | if err != nil { 38 | RespError(w, RespInternalErr, err) 39 | return 40 | } 41 | 42 | // fmt.Println(data) 43 | switch dbdata.StructName(data) { 44 | case "SettingSmtp": 45 | // 密码为空时则不修改 46 | smtp := &dbdata.SettingSmtp{} 47 | err := dbdata.SettingGet(smtp) 48 | if err == nil && data.(*dbdata.SettingSmtp).Password == "" { 49 | data.(*dbdata.SettingSmtp).Password = smtp.Password 50 | } 51 | } 52 | err = dbdata.SettingSet(data) 53 | if err != nil { 54 | RespError(w, RespInternalErr, err) 55 | return 56 | } 57 | RespSucess(w, data) 58 | } 59 | 60 | func SetOtherSmtp(w http.ResponseWriter, r *http.Request) { 61 | data := &dbdata.SettingSmtp{} 62 | setOtherGet(data, w) 63 | } 64 | 65 | func SetOtherSmtpEdit(w http.ResponseWriter, r *http.Request) { 66 | data := &dbdata.SettingSmtp{} 67 | setOtherEdit(data, w, r) 68 | } 69 | 70 | func SetOther(w http.ResponseWriter, r *http.Request) { 71 | data := &dbdata.SettingOther{} 72 | setOtherGet(data, w) 73 | } 74 | 75 | func SetOtherEdit(w http.ResponseWriter, r *http.Request) { 76 | data := &dbdata.SettingOther{} 77 | setOtherEdit(data, w, r) 78 | } 79 | 80 | func SetOtherAuditLog(w http.ResponseWriter, r *http.Request) { 81 | data, err := dbdata.SettingGetAuditLog() 82 | if err != nil { 83 | RespError(w, RespInternalErr, err) 84 | return 85 | } 86 | data.AuditInterval = base.Cfg.AuditInterval 87 | RespSucess(w, data) 88 | } 89 | 90 | func SetOtherAuditLogEdit(w http.ResponseWriter, r *http.Request) { 91 | body, err := io.ReadAll(r.Body) 92 | if err != nil { 93 | RespError(w, RespInternalErr, err) 94 | return 95 | } 96 | defer r.Body.Close() 97 | data := &dbdata.SettingAuditLog{} 98 | err = json.Unmarshal(body, data) 99 | if err != nil { 100 | RespError(w, RespInternalErr, err) 101 | return 102 | } 103 | if data.LifeDay < 0 || data.LifeDay > 365 { 104 | RespError(w, RespParamErr, errors.New("日志存储时长范围在 0 ~ 365")) 105 | return 106 | } 107 | ok, _ := regexp.Match("^([0-9]|0[0-9]|1[0-9]|2[0-3]):([0][0])$", []byte(data.ClearTime)) 108 | if !ok { 109 | RespError(w, RespParamErr, errors.New("每天清理时间填写有误")) 110 | return 111 | } 112 | err = dbdata.SettingSet(data) 113 | if err != nil { 114 | RespError(w, RespInternalErr, err) 115 | return 116 | } 117 | RespSucess(w, data) 118 | } 119 | -------------------------------------------------------------------------------- /server/admin/api_policy.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/bjdgyc/anylink/dbdata" 10 | ) 11 | 12 | func PolicyList(w http.ResponseWriter, r *http.Request) { 13 | _ = r.ParseForm() 14 | pageS := r.FormValue("page") 15 | page, _ := strconv.Atoi(pageS) 16 | if page < 1 { 17 | page = 1 18 | } 19 | 20 | var pageSize = dbdata.PageSize 21 | 22 | count := dbdata.CountAll(&dbdata.Policy{}) 23 | 24 | var datas []dbdata.Policy 25 | err := dbdata.Find(&datas, pageSize, page) 26 | if err != nil { 27 | RespError(w, RespInternalErr, err) 28 | return 29 | } 30 | 31 | data := map[string]interface{}{ 32 | "count": count, 33 | "page_size": pageSize, 34 | "datas": datas, 35 | } 36 | 37 | RespSucess(w, data) 38 | } 39 | 40 | func PolicyDetail(w http.ResponseWriter, r *http.Request) { 41 | _ = r.ParseForm() 42 | idS := r.FormValue("id") 43 | id, _ := strconv.Atoi(idS) 44 | if id < 1 { 45 | RespError(w, RespParamErr, "Id错误") 46 | return 47 | } 48 | 49 | var data dbdata.Policy 50 | err := dbdata.One("Id", id, &data) 51 | if err != nil { 52 | RespError(w, RespInternalErr, err) 53 | return 54 | } 55 | 56 | RespSucess(w, data) 57 | } 58 | 59 | func PolicySet(w http.ResponseWriter, r *http.Request) { 60 | body, err := io.ReadAll(r.Body) 61 | if err != nil { 62 | RespError(w, RespInternalErr, err) 63 | return 64 | } 65 | defer r.Body.Close() 66 | v := &dbdata.Policy{} 67 | err = json.Unmarshal(body, v) 68 | if err != nil { 69 | RespError(w, RespInternalErr, err) 70 | return 71 | } 72 | 73 | err = dbdata.SetPolicy(v) 74 | if err != nil { 75 | RespError(w, RespInternalErr, err) 76 | return 77 | } 78 | 79 | RespSucess(w, nil) 80 | } 81 | 82 | func PolicyDel(w http.ResponseWriter, r *http.Request) { 83 | _ = r.ParseForm() 84 | idS := r.FormValue("id") 85 | id, _ := strconv.Atoi(idS) 86 | if id < 1 { 87 | RespError(w, RespParamErr, "Id错误") 88 | return 89 | } 90 | 91 | data := dbdata.Policy{Id: id} 92 | err := dbdata.Del(&data) 93 | if err != nil { 94 | RespError(w, RespInternalErr, err) 95 | return 96 | } 97 | RespSucess(w, nil) 98 | } 99 | -------------------------------------------------------------------------------- /server/admin/api_set.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "runtime" 7 | 8 | "github.com/bjdgyc/anylink/base" 9 | "github.com/bjdgyc/anylink/dbdata" 10 | "github.com/bjdgyc/anylink/pkg/utils" 11 | "github.com/bjdgyc/anylink/sessdata" 12 | "github.com/shirou/gopsutil/cpu" 13 | "github.com/shirou/gopsutil/disk" 14 | "github.com/shirou/gopsutil/host" 15 | "github.com/shirou/gopsutil/load" 16 | "github.com/shirou/gopsutil/mem" 17 | ) 18 | 19 | func SetHome(w http.ResponseWriter, r *http.Request) { 20 | data := make(map[string]interface{}) 21 | 22 | sess := sessdata.OnlineSess() 23 | 24 | data["counts"] = map[string]int{ 25 | "online": len(sess), 26 | "user": dbdata.CountAll(&dbdata.User{}), 27 | "group": dbdata.CountAll(&dbdata.Group{}), 28 | "ip_map": dbdata.CountAll(&dbdata.IpMap{}), 29 | } 30 | 31 | RespSucess(w, data) 32 | } 33 | 34 | func SetSystem(w http.ResponseWriter, r *http.Request) { 35 | data := make(map[string]interface{}) 36 | 37 | m, _ := mem.VirtualMemory() 38 | data["mem"] = map[string]interface{}{ 39 | "total": utils.HumanByte(m.Total), 40 | "free": utils.HumanByte(m.Free), 41 | "percent": decimal(m.UsedPercent), 42 | } 43 | 44 | d, _ := disk.Usage("/") 45 | data["disk"] = map[string]interface{}{ 46 | "total": utils.HumanByte(d.Total), 47 | "free": utils.HumanByte(d.Free), 48 | "percent": decimal(d.UsedPercent), 49 | } 50 | 51 | cc, _ := cpu.Counts(true) 52 | c, _ := cpu.Info() 53 | ci := c[0] 54 | cpuUsedPercent, _ := cpu.Percent(0, false) 55 | cup := cpuUsedPercent[0] 56 | if cup == 0 { 57 | cup = 1 58 | } 59 | data["cpu"] = map[string]interface{}{ 60 | "core": cc, 61 | "modelName": ci.ModelName, 62 | "ghz": fmt.Sprintf("%.2f GHz", ci.Mhz/1000), 63 | "percent": decimal(cup), 64 | } 65 | 66 | hi, _ := host.Info() 67 | l, _ := load.Avg() 68 | data["sys"] = map[string]interface{}{ 69 | "goOs": runtime.GOOS, 70 | "goArch": runtime.GOARCH, 71 | "goVersion": runtime.Version(), 72 | "goroutine": runtime.NumGoroutine(), 73 | "appVersion": "v" + base.APP_VER, 74 | "appCommitId": base.CommitId, 75 | "appBuildDate": base.BuildDate, 76 | 77 | "hostname": hi.Hostname, 78 | "platform": fmt.Sprintf("%v %v %v", hi.Platform, hi.PlatformFamily, hi.PlatformVersion), 79 | "kernel": hi.KernelVersion, 80 | 81 | "load": fmt.Sprint(l.Load1, l.Load5, l.Load15), 82 | } 83 | 84 | RespSucess(w, data) 85 | } 86 | 87 | func SetSoft(w http.ResponseWriter, r *http.Request) { 88 | data := base.ServerCfg2Slice() 89 | RespSucess(w, data) 90 | } 91 | 92 | func decimal(f float64) float64 { 93 | i := int(f * 100) 94 | return float64(i) / 100 95 | } 96 | -------------------------------------------------------------------------------- /server/admin/api_set_audit.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/bjdgyc/anylink/dbdata" 8 | "github.com/gocarina/gocsv" 9 | ) 10 | 11 | func SetAuditList(w http.ResponseWriter, r *http.Request) { 12 | _ = r.ParseForm() 13 | pageS := r.FormValue("page") 14 | page, _ := strconv.Atoi(pageS) 15 | if page < 1 { 16 | page = 1 17 | } 18 | var datas []dbdata.AccessAudit 19 | session := dbdata.GetAuditSession(r.FormValue("search")) 20 | count, err := dbdata.FindAndCount(session, &datas, dbdata.PageSize, page) 21 | if err != nil && !dbdata.CheckErrNotFound(err) { 22 | RespError(w, RespInternalErr, err) 23 | return 24 | } 25 | data := map[string]interface{}{ 26 | "count": count, 27 | "page_size": dbdata.PageSize, 28 | "datas": datas, 29 | } 30 | 31 | RespSucess(w, data) 32 | } 33 | 34 | func SetAuditExport(w http.ResponseWriter, r *http.Request) { 35 | var datas []dbdata.AccessAudit 36 | maxNum := 1000000 37 | session := dbdata.GetAuditSession(r.FormValue("search")) 38 | count, err := dbdata.FindAndCount(session, &datas, maxNum, 0) 39 | if err != nil && !dbdata.CheckErrNotFound(err) { 40 | RespError(w, RespInternalErr, err) 41 | return 42 | } 43 | if count == 0 { 44 | RespError(w, RespParamErr, "你导出的总数量为0条,请调整搜索条件,再导出") 45 | return 46 | } 47 | if count > int64(maxNum) { 48 | RespError(w, RespParamErr, "你导出的数据量超过100万条,请调整搜索条件,再导出") 49 | return 50 | } 51 | gocsv.Marshal(datas, w) 52 | 53 | } 54 | 55 | func UserActLogList(w http.ResponseWriter, r *http.Request) { 56 | _ = r.ParseForm() 57 | pageS := r.FormValue("page") 58 | page, _ := strconv.Atoi(pageS) 59 | if page < 1 { 60 | page = 1 61 | } 62 | var datas []dbdata.UserActLog 63 | session := dbdata.UserActLogIns.GetSession(r.Form) 64 | count, err := dbdata.FindAndCount(session, &datas, dbdata.PageSize, page) 65 | if err != nil && !dbdata.CheckErrNotFound(err) { 66 | RespError(w, RespInternalErr, err) 67 | return 68 | } 69 | data := map[string]interface{}{ 70 | "count": count, 71 | "page_size": dbdata.PageSize, 72 | "datas": datas, 73 | "statusOps": dbdata.UserActLogIns.GetStatusOpsWithTag(), 74 | "osOps": dbdata.UserActLogIns.OsOps, 75 | "clientOps": dbdata.UserActLogIns.ClientOps, 76 | } 77 | 78 | RespSucess(w, data) 79 | } 80 | -------------------------------------------------------------------------------- /server/admin/api_statsinfo.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/bjdgyc/anylink/dbdata" 8 | ) 9 | 10 | func StatsInfoList(w http.ResponseWriter, r *http.Request) { 11 | var ok bool 12 | _ = r.ParseForm() 13 | action := r.FormValue("action") 14 | scope := r.FormValue("scope") 15 | ok = dbdata.StatsInfoIns.ValidAction(action) 16 | if !ok { 17 | RespError(w, RespParamErr, errors.New("不存在的图表类别")) 18 | return 19 | } 20 | ok = dbdata.StatsInfoIns.ValidScope(scope) 21 | if !ok { 22 | RespError(w, RespParamErr, errors.New("不存在的日期范围")) 23 | return 24 | } 25 | datas, err := dbdata.StatsInfoIns.GetData(action, scope) 26 | if err != nil { 27 | RespError(w, RespInternalErr, err) 28 | return 29 | } 30 | data := make(map[string]interface{}) 31 | data["datas"] = datas 32 | RespSucess(w, data) 33 | } 34 | -------------------------------------------------------------------------------- /server/admin/api_uploaduser.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | "path" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/bjdgyc/anylink/dbdata" 14 | "github.com/bjdgyc/anylink/pkg/utils" 15 | mapset "github.com/deckarep/golang-set" 16 | "github.com/spf13/cast" 17 | "github.com/xuri/excelize/v2" 18 | ) 19 | 20 | func UserUpload(w http.ResponseWriter, r *http.Request) { 21 | r.ParseMultipartForm(8 << 20) 22 | file, header, err := r.FormFile("file") 23 | if err != nil || !strings.Contains(header.Filename, ".xlsx") || !strings.Contains(header.Filename, ".xls") { 24 | RespError(w, RespInternalErr, "文件解析失败:仅支持xlsx或xls文件") 25 | return 26 | } 27 | defer file.Close() 28 | 29 | // go/path-injection 30 | // base.Cfg.FilesPath 可以直接对外访问,不能上传文件到此 31 | fileName := path.Join(os.TempDir(), utils.RandomRunes(10)) 32 | newFile, err := os.Create(fileName) 33 | if err != nil { 34 | RespError(w, RespInternalErr, "创建文件失败:", err) 35 | return 36 | } 37 | defer newFile.Close() 38 | 39 | io.Copy(newFile, file) 40 | if err = UploadUser(newFile.Name()); err != nil { 41 | RespError(w, RespInternalErr, err) 42 | os.Remove(fileName) 43 | return 44 | } 45 | os.Remove(fileName) 46 | RespSucess(w, "批量添加成功") 47 | } 48 | 49 | func UploadUser(file string) error { 50 | f, err := excelize.OpenFile(file) 51 | if err != nil { 52 | return err 53 | } 54 | defer func() { 55 | if err := f.Close(); err != nil { 56 | return 57 | } 58 | }() 59 | rows, err := f.GetRows("Sheet1") 60 | if err != nil { 61 | return err 62 | } 63 | if rows[0][0] != "id" || rows[0][1] != "username" || rows[0][2] != "nickname" || rows[0][3] != "email" || rows[0][4] != "pin_code" || rows[0][5] != "limittime" || rows[0][6] != "otp_secret" || rows[0][7] != "disable_otp" || rows[0][8] != "groups" || rows[0][9] != "status" || rows[0][10] != "send_email" { 64 | return fmt.Errorf("批量添加失败,表格格式不正确") 65 | } 66 | var k []interface{} 67 | for _, v := range dbdata.GetGroupNames() { 68 | k = append(k, v) 69 | } 70 | for index, row := range rows { 71 | if index == 0 { 72 | continue 73 | } 74 | id, _ := strconv.Atoi(row[0]) 75 | if len(row[4]) < 6 { 76 | row[4] = utils.RandomRunes(8) 77 | } 78 | limittime, _ := time.ParseInLocation("2006-01-02 15:04:05", row[5], time.Local) 79 | disableOtp, _ := strconv.ParseBool(row[7]) 80 | var group []string 81 | if row[8] == "" { 82 | return fmt.Errorf("第%d行数据错误,用户组不允许为空", index) 83 | } 84 | for _, v := range strings.Split(row[8], ",") { 85 | if s := mapset.NewSetFromSlice(k); s.Contains(v) { 86 | group = append(group, v) 87 | } else { 88 | return fmt.Errorf("用户组【%s】不存在,请检查第%d行数据", v, index) 89 | } 90 | } 91 | status := cast.ToInt8(row[9]) 92 | sendmail, _ := strconv.ParseBool(row[10]) 93 | // createdAt, _ := time.ParseInLocation("2006-01-02 15:04:05", row[11], time.Local) 94 | // updatedAt, _ := time.ParseInLocation("2006-01-02 15:04:05", row[12], time.Local) 95 | user := &dbdata.User{ 96 | Id: id, 97 | Username: row[1], 98 | Nickname: row[2], 99 | Email: row[3], 100 | PinCode: row[4], 101 | LimitTime: &limittime, 102 | OtpSecret: row[6], 103 | DisableOtp: disableOtp, 104 | Groups: group, 105 | Status: status, 106 | SendEmail: sendmail, 107 | // CreatedAt: createdAt, 108 | // UpdatedAt: updatedAt, 109 | } 110 | if err := dbdata.AddBatch(user); err != nil { 111 | return fmt.Errorf("请检查第%d行数据是否导入有重复用户", index) 112 | } 113 | user.PinCode = row[4] 114 | if user.SendEmail { 115 | if err := userAccountMail(user); err != nil { 116 | return err 117 | } 118 | } 119 | } 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /server/admin/common.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "time" 7 | 8 | "github.com/bjdgyc/anylink/base" 9 | "github.com/bjdgyc/anylink/dbdata" 10 | "github.com/golang-jwt/jwt/v4" 11 | mail "github.com/xhit/go-simple-mail/v2" 12 | // "github.com/mojocn/base64Captcha" 13 | ) 14 | 15 | func SetJwtData(data map[string]interface{}, expiresAt int64) (string, error) { 16 | jwtData := jwt.MapClaims{"exp": expiresAt} 17 | for k, v := range data { 18 | jwtData[k] = v 19 | } 20 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwtData) 21 | 22 | // Sign and get the complete encoded token as a string using the secret 23 | tokenString, err := token.SignedString([]byte(base.Cfg.JwtSecret)) 24 | return tokenString, err 25 | } 26 | 27 | func GetJwtData(jwtToken string) (map[string]interface{}, error) { 28 | token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) { 29 | // since we only use the one private key to sign the tokens, 30 | // we also only use its public counter part to verify 31 | return []byte(base.Cfg.JwtSecret), nil 32 | }) 33 | 34 | if err != nil || !token.Valid { 35 | return nil, err 36 | } 37 | 38 | claims, ok := token.Claims.(jwt.MapClaims) 39 | if !ok { 40 | return nil, errors.New("data is parse err") 41 | } 42 | 43 | return claims, nil 44 | } 45 | 46 | func SendMail(subject, to, htmlBody string, attach *mail.File) error { 47 | 48 | dataSmtp := &dbdata.SettingSmtp{} 49 | err := dbdata.SettingGet(dataSmtp) 50 | if err != nil { 51 | base.Error(err) 52 | return err 53 | } 54 | 55 | server := mail.NewSMTPClient() 56 | 57 | // SMTP Server 58 | server.Host = dataSmtp.Host 59 | server.Port = dataSmtp.Port 60 | server.Username = dataSmtp.Username 61 | server.Password = dataSmtp.Password 62 | 63 | switch dataSmtp.Encryption { 64 | case "SSLTLS": 65 | server.Encryption = mail.EncryptionSSLTLS 66 | case "STARTTLS": 67 | server.Encryption = mail.EncryptionSTARTTLS 68 | default: 69 | server.Encryption = mail.EncryptionNone 70 | } 71 | 72 | // Since v2.3.0 you can specified authentication type: 73 | // - PLAIN (default) 74 | // - LOGIN 75 | // - CRAM-MD5 76 | server.Authentication = mail.AuthAuto 77 | 78 | // Variable to keep alive connection 79 | server.KeepAlive = false 80 | 81 | // Timeout for connect to SMTP Server 82 | server.ConnectTimeout = 10 * time.Second 83 | 84 | // Timeout for send the data and wait respond 85 | server.SendTimeout = 10 * time.Second 86 | 87 | // Set TLSConfig to provide custom TLS configuration. For example, 88 | // to skip TLS verification (useful for testing): 89 | server.TLSConfig = &tls.Config{InsecureSkipVerify: true} 90 | 91 | // SMTP client 92 | smtpClient, err := server.Connect() 93 | 94 | if err != nil { 95 | base.Error(err) 96 | return err 97 | } 98 | 99 | // New email simple html with inline and CC 100 | email := mail.NewMSG() 101 | email.SetFrom(dataSmtp.From). 102 | AddTo(to). 103 | SetSubject(subject) 104 | 105 | if attach != nil { 106 | email.Attach(attach) 107 | } 108 | 109 | email.SetBody(mail.TextHTML, htmlBody) 110 | 111 | // Call Send and pass the client 112 | err = email.Send(smtpClient) 113 | if err != nil { 114 | base.Error(err) 115 | } 116 | 117 | return err 118 | } 119 | -------------------------------------------------------------------------------- /server/admin/common_test.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/bjdgyc/anylink/base" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestJwtData(t *testing.T) { 12 | assert := assert.New(t) 13 | base.Cfg.JwtSecret = "dsfasfdfsadfasdfasd3sdaf" 14 | data := map[string]interface{}{ 15 | "key": "value", 16 | } 17 | expiresAt := time.Now().Add(time.Minute).Unix() 18 | token, err := SetJwtData(data, expiresAt) 19 | assert.Nil(err) 20 | dataN, err := GetJwtData(token) 21 | assert.Nil(err) 22 | assert.Equal(dataN["key"], "value") 23 | } 24 | -------------------------------------------------------------------------------- /server/admin/error.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | // 返回码 4 | const ( 5 | RespSuccess = 0 6 | RespInternalErr = 1 7 | RespTokenErr = 2 8 | RespUserOrPassErr = 3 9 | RespParamErr = 4 10 | ) 11 | 12 | var RespMap = map[int]string{ 13 | RespTokenErr: "客户端TOKEN错误", 14 | RespUserOrPassErr: "用户名或密码错误", 15 | } 16 | -------------------------------------------------------------------------------- /server/admin/resp.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "runtime" 8 | 9 | "github.com/bjdgyc/anylink/base" 10 | ) 11 | 12 | type Resp struct { 13 | Code int `json:"code"` 14 | Msg string `json:"msg"` 15 | Location string `json:"location"` 16 | Data interface{} `json:"data"` 17 | } 18 | 19 | func respHttp(w http.ResponseWriter, respCode int, data interface{}, errS ...interface{}) { 20 | resp := Resp{ 21 | Code: respCode, 22 | Msg: "success", 23 | Data: data, 24 | } 25 | _, file, line, _ := runtime.Caller(2) 26 | resp.Location = fmt.Sprintf("%v:%v", file, line) 27 | 28 | if respCode != 0 { 29 | resp.Msg = "" 30 | if v, ok := RespMap[respCode]; ok { 31 | resp.Msg += v 32 | } 33 | 34 | if len(errS) > 0 { 35 | resp.Msg += fmt.Sprint(errS...) 36 | } 37 | } 38 | 39 | b, err := json.Marshal(resp) 40 | if err != nil { 41 | base.Error(err, resp) 42 | } 43 | 44 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 45 | w.WriteHeader(http.StatusOK) 46 | _, err = w.Write(b) 47 | if err != nil { 48 | base.Error(err) 49 | } 50 | // 记录返回数据 51 | // logger.Category("response").Debug(string(b)) 52 | } 53 | 54 | func RespSucess(w http.ResponseWriter, data interface{}) { 55 | respHttp(w, 0, data, "") 56 | } 57 | 58 | func RespError(w http.ResponseWriter, respCode int, errS ...interface{}) { 59 | respHttp(w, respCode, nil, errS...) 60 | } 61 | 62 | func RespData(w http.ResponseWriter, data interface{}, err error) { 63 | respHttp(w, http.StatusOK, data, "") 64 | } 65 | -------------------------------------------------------------------------------- /server/admin/resp_test.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestRespSucess(t *testing.T) { 13 | assert := assert.New(t) 14 | w := httptest.NewRecorder() 15 | RespSucess(w, "data") 16 | // fmt.Println(w) 17 | assert.Equal(w.Code, 200) 18 | body, _ := io.ReadAll(w.Body) 19 | res := Resp{} 20 | err := json.Unmarshal(body, &res) 21 | assert.Nil(err) 22 | assert.Equal(res.Code, 0) 23 | assert.Equal(res.Data, "data") 24 | 25 | } 26 | 27 | func TestRespError(t *testing.T) { 28 | assert := assert.New(t) 29 | w := httptest.NewRecorder() 30 | RespError(w, 10, "err-msg") 31 | // fmt.Println(w) 32 | assert.Equal(w.Code, 200) 33 | body, _ := io.ReadAll(w.Body) 34 | res := Resp{} 35 | err := json.Unmarshal(body, &res) 36 | assert.Nil(err) 37 | assert.Equal(res.Code, 10) 38 | assert.Equal(res.Msg, "err-msg") 39 | } 40 | -------------------------------------------------------------------------------- /server/base/app_ver.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | const ( 4 | APP_NAME = "AnyLink" 5 | ) 6 | 7 | var ( 8 | // APP_VER app版本号 9 | APP_VER = "0.0.1" 10 | // 提交id 11 | CommitId string 12 | BuildDate string 13 | ) 14 | -------------------------------------------------------------------------------- /server/base/log.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | const ( 13 | LogLevelTrace = iota 14 | LogLevelDebug 15 | LogLevelInfo 16 | LogLevelWarn 17 | LogLevelError 18 | LogLevelFatal 19 | ) 20 | 21 | var ( 22 | baseLw *logWriter 23 | baseLog *log.Logger 24 | baseLevel int 25 | levels map[int]string 26 | 27 | dateFormat = "2006-01-02" 28 | logName = "anylink.log" 29 | ) 30 | 31 | func init() { 32 | log.SetFlags(log.LstdFlags | log.Lshortfile) 33 | } 34 | 35 | // 实现 os.Writer 接口 36 | type logWriter struct { 37 | UseStdout bool 38 | FileName string 39 | File *os.File 40 | NowDate string 41 | } 42 | 43 | // 实现日志文件的切割 44 | func (lw *logWriter) Write(p []byte) (n int, err error) { 45 | if !lw.UseStdout { 46 | return lw.File.Write(p) 47 | } 48 | 49 | date := time.Now().Format(dateFormat) 50 | if lw.NowDate != date { 51 | _ = lw.File.Close() 52 | _ = os.Rename(lw.FileName, lw.FileName+"."+lw.NowDate) 53 | lw.NowDate = date 54 | lw.newFile() 55 | } 56 | return lw.File.Write(p) 57 | } 58 | 59 | // 创建新文件 60 | func (lw *logWriter) newFile() { 61 | if lw.UseStdout { 62 | lw.File = os.Stdout 63 | return 64 | } 65 | 66 | f, err := os.OpenFile(lw.FileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) 67 | if err != nil { 68 | panic(err) 69 | } 70 | lw.File = f 71 | } 72 | 73 | func initLog() { 74 | // 初始化 baseLog 75 | baseLw = &logWriter{ 76 | UseStdout: Cfg.LogPath == "", 77 | FileName: path.Join(Cfg.LogPath, logName), 78 | NowDate: time.Now().Format(dateFormat), 79 | } 80 | 81 | baseLw.newFile() 82 | baseLevel = logLevel2Int(Cfg.LogLevel) 83 | baseLog = log.New(baseLw, "", log.LstdFlags|log.Lshortfile) 84 | 85 | serverLog = log.New(&sLogWriter{}, "[http_server]", log.LstdFlags|log.Lshortfile) 86 | } 87 | 88 | func GetBaseLw() *logWriter { 89 | return baseLw 90 | } 91 | 92 | var serverLog *log.Logger 93 | 94 | type sLogWriter struct{} 95 | 96 | func (w *sLogWriter) Write(p []byte) (n int, err error) { 97 | if Cfg.HttpServerLog { 98 | return os.Stderr.Write(p) 99 | } 100 | return 0, nil 101 | } 102 | 103 | // 获取 log.Logger 104 | func GetServerLog() *log.Logger { 105 | return serverLog 106 | } 107 | 108 | func GetLogLevel() int { 109 | return baseLevel 110 | } 111 | 112 | func logLevel2Int(l string) int { 113 | levels = map[int]string{ 114 | LogLevelTrace: "Trace", 115 | LogLevelDebug: "Debug", 116 | LogLevelInfo: "Info", 117 | LogLevelWarn: "Warn", 118 | LogLevelError: "Error", 119 | LogLevelFatal: "Fatal", 120 | } 121 | lvl := LogLevelInfo 122 | for k, v := range levels { 123 | if strings.ToLower(l) == strings.ToLower(v) { 124 | lvl = k 125 | } 126 | } 127 | return lvl 128 | } 129 | 130 | func output(l int, s ...interface{}) { 131 | lvl := fmt.Sprintf("[%s] ", levels[l]) 132 | _ = baseLog.Output(3, lvl+fmt.Sprintln(s...)) 133 | } 134 | 135 | func Trace(v ...interface{}) { 136 | l := LogLevelTrace 137 | if baseLevel > l { 138 | return 139 | } 140 | output(l, v...) 141 | } 142 | 143 | func Debug(v ...interface{}) { 144 | l := LogLevelDebug 145 | if baseLevel > l { 146 | return 147 | } 148 | output(l, v...) 149 | } 150 | 151 | func Info(v ...interface{}) { 152 | l := LogLevelInfo 153 | if baseLevel > l { 154 | return 155 | } 156 | output(l, v...) 157 | } 158 | 159 | func Warn(v ...interface{}) { 160 | l := LogLevelWarn 161 | if baseLevel > l { 162 | return 163 | } 164 | output(l, v...) 165 | } 166 | 167 | func Error(v ...interface{}) { 168 | l := LogLevelError 169 | if baseLevel > l { 170 | return 171 | } 172 | output(l, v...) 173 | } 174 | 175 | func Fatal(v ...interface{}) { 176 | l := LogLevelFatal 177 | if baseLevel > l { 178 | return 179 | } 180 | output(l, v...) 181 | os.Exit(1) 182 | } 183 | -------------------------------------------------------------------------------- /server/base/mod.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | procModulesPath = "/proc/modules" 14 | inContainerKey = "ANYLINK_IN_CONTAINER" 15 | tunPath = "/dev/net/tun" 16 | ) 17 | 18 | var ( 19 | InContainer = false 20 | modMap = map[string]struct{}{} 21 | ) 22 | 23 | func initMod() { 24 | container := os.Getenv(inContainerKey) 25 | if container == "on" { 26 | InContainer = true 27 | } 28 | log.Println("InContainer", InContainer) 29 | 30 | file, err := os.Open(procModulesPath) 31 | if err != nil { 32 | err = fmt.Errorf("[ERROR] Problem with open file: %s", err) 33 | panic(err) 34 | } 35 | defer file.Close() 36 | scanner := bufio.NewScanner(file) 37 | scanner.Split(bufio.ScanLines) 38 | for scanner.Scan() { 39 | splited := strings.Split(scanner.Text(), " ") 40 | if len(splited[0]) > 0 { 41 | modMap[splited[0]] = struct{}{} 42 | } 43 | } 44 | } 45 | 46 | func CheckModOrLoad(mod string) { 47 | log.Println("CheckModOrLoad", mod) 48 | 49 | if _, ok := modMap[mod]; ok { 50 | return 51 | } 52 | 53 | var err error 54 | 55 | if mod == "tun" || mod == "tap" { 56 | _, err = os.Stat(tunPath) 57 | if err == nil { 58 | // 文件存在 59 | return 60 | } 61 | // err = fmt.Errorf("[error] Linux tunFile is null %s", tunPath) 62 | // log.Println(err) 63 | // return 64 | } 65 | 66 | if InContainer { 67 | err = fmt.Errorf("[error] Linux module %s is not loaded, please run `modprobe %s`", mod, mod) 68 | log.Println(err) 69 | return 70 | // panic(err) 71 | } 72 | 73 | cmdstr := fmt.Sprintln("modprobe", mod) 74 | 75 | cmd := exec.Command("sh", "-c", cmdstr) 76 | b, err := cmd.CombinedOutput() 77 | if err != nil { 78 | log.Println(mod, string(b)) 79 | panic(err) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /server/base/start.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | func Start() { 4 | execute() 5 | initCfg() 6 | initLog() 7 | initMod() 8 | } 9 | 10 | func Test() { 11 | initLog() 12 | } 13 | -------------------------------------------------------------------------------- /server/bridge-init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #yum install bridge-utils 4 | 5 | # Define Bridge Interface 6 | br="anylink0" 7 | 8 | # 请根据sever服务器信息,更新下面的信息 9 | eth="eth0" 10 | eth_ip="192.168.10.4/24" 11 | eth_broadcast="192.168.10.255" 12 | eth_gateway="192.168.10.1" 13 | 14 | 15 | brctl addbr $br 16 | brctl addif $br $eth 17 | 18 | ip addr del $eth_ip dev $eth 19 | ip addr add 0.0.0.0 dev $eth 20 | ip link set dev $eth up promisc on 21 | 22 | mac=`cat /sys/class/net/$eth/address` 23 | ip link set dev $br up address $mac promisc on 24 | ip addr add $eth_ip broadcast $eth_broadcast dev $br 25 | 26 | 27 | route add default gateway $eth_gateway 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /server/conf/files/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjdgyc/anylink/805498e270a2172f116161f3534cfcf76de2f83d/server/conf/files/index.html -------------------------------------------------------------------------------- /server/conf/files/info.txt: -------------------------------------------------------------------------------- 1 | 客户端软件需放置在files目录内, 2 | 如需要帮助请加QQ群:567510628 、739072205 -------------------------------------------------------------------------------- /server/conf/profile.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | false 7 | false 8 | false 9 | IPSec 10 | true 11 | false 12 | true 13 | AllowRemoteUsers 14 | AllowRemoteUsers 15 | pinAllowed 16 | 17 | 18 | Digital_Signature 19 | 20 | 21 | ClientAuth 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | VPN 31 | localhost 32 | 33 | 34 | 35 | VPN2 36 | localhost2 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /server/conf/server-sample.toml: -------------------------------------------------------------------------------- 1 | #示例配置信息 2 | 3 | #其他配置文件,可以使用绝对路径 4 | #或者相对于 anylink 二进制文件的路径 5 | 6 | #数据文件 7 | db_type = "sqlite3" 8 | db_source = "./conf/anylink.db" 9 | #证书文件 使用跟nginx一样的证书即可 10 | cert_file = "./conf/vpn_cert.pem" 11 | cert_key = "./conf/vpn_cert.key" 12 | files_path = "./conf/files" 13 | profile = "./conf/profile.xml" 14 | #profile name(用于区分不同服务端的配置) 15 | #客户端存放位置 16 | #Windows 10 17 | #%ProgramData%Cisco\Cisco AnyConnect Secure Mobility Client\Profile 18 | #Mac Os X 19 | #/opt/cisco/anyconnect/profile 20 | #Linux 21 | #/opt/cisco/anyconnect/profile 22 | profile_name = "anylink" 23 | #日志目录,默认为空写入标准输出 24 | #log_path = "./log" 25 | log_path = "" 26 | log_level = "debug" 27 | pprof = true 28 | 29 | #系统名称 30 | issuer = "XX公司VPN" 31 | #后台管理用户 32 | admin_user = "admin" 33 | #pass 123456 34 | admin_pass = "$2a$10$UQ7C.EoPifDeJh6d8.31TeSPQU7hM/NOM2nixmBucJpAuXDQNqNke" 35 | # 留空表示不开启 otp, 开启otp后密码为 pass + 6位otp 36 | # 生成 ./anylink tool -o 37 | admin_otp = "" 38 | jwt_secret = "abcdef.0123456789.abcdef" 39 | 40 | 41 | #TCP服务监听地址(任意端口) 42 | server_addr = ":443" 43 | #开启 DTLS 44 | server_dtls = false 45 | #UDP监听地址(任意端口) 46 | server_dtls_addr = ":443" 47 | #DTLS对外映射端口(为空则与server_dtls_addr相同) 48 | advertise_dtls_addr = "" 49 | #后台服务监听地址 50 | admin_addr = ":8800" 51 | #开启tcp proxy protocol协议 52 | proxy_protocol = false 53 | 54 | #开启go标准库http.Server的日志 55 | http_server_log=false 56 | 57 | #虚拟网络类型[tun macvtap tap] 58 | link_mode = "tun" 59 | 60 | #客户端分配的ip地址池 61 | #docker环境一般默认 eth0,其他情况根据实际网卡信息填写 62 | ipv4_master = "eth0" 63 | ipv4_cidr = "192.168.90.0/24" 64 | ipv4_gateway = "192.168.90.1" 65 | ipv4_start = "192.168.90.100" 66 | ipv4_end = "192.168.90.200" 67 | 68 | #最大客户端数量 69 | max_client = 200 70 | #单个用户同时在线数量 71 | max_user_client = 3 72 | #IP租期(秒) 73 | ip_lease = 86400 74 | 75 | #默认选择的组 76 | default_group = "one" 77 | 78 | #客户端失效检测时间(秒) dpd > keepalive 79 | cstp_keepalive = 3 80 | cstp_dpd = 20 81 | mobile_keepalive = 4 82 | mobile_dpd = 60 83 | 84 | # 根据实际情况修改 85 | #cstp_keepalive = 20 86 | #cstp_dpd = 30 87 | #mobile_keepalive = 40 88 | #mobile_dpd = 60 89 | 90 | #设置最大传输单元 91 | mtu = 1460 92 | 93 | # 客户端dns的默认搜索域 94 | default_domain = "example.com" 95 | #default_domain = "example.com abc.example.com" 96 | 97 | #空闲链接超时时间(秒)-超时后断开链接,0关闭此功能 98 | idle_timeout = 0 99 | #session过期时间,用于断线重连,0永不过期 100 | session_timeout = 3600 101 | #auth_timeout = 0 102 | audit_interval = 600 103 | 104 | show_sql = false 105 | 106 | #是否自动添加nat 107 | iptables_nat = true 108 | 109 | #启用压缩 110 | compression = false 111 | #低于及等于多少字节不压缩 112 | no_compress_limit = 256 113 | 114 | #客户端显示详细错误信息(线上环境慎开启) 115 | display_error = false 116 | 117 | #排除出口ip路由(出口ip不加密传输) 118 | exclude_export_ip = true 119 | 120 | #登录单独验证OTP窗口 121 | auth_alone_otp = false 122 | 123 | #加密保存用户密码 124 | encryption_password = false 125 | 126 | #防爆破全局开关 127 | anti_brute_force = true 128 | #全局IP白名单,多个用逗号分隔,支持单IP和CIDR范围 129 | ip_whitelist = "192.168.90.1,172.16.0.0/24" 130 | 131 | #锁定时间最好不要超过单位时间 132 | #单位时间内最大尝试次数,0为关闭该功能 133 | max_ban_score = 5 134 | #设置单位时间(秒),超过则重置计数 135 | ban_reset_time = 600 136 | #超过最大尝试次数后的锁定时长(秒) 137 | lock_time = 300 138 | 139 | #全局用户单位时间内最大尝试次数,0为关闭该功能 140 | max_global_user_ban_count = 20 141 | #全局用户设置单位时间(秒) 142 | global_user_ban_reset_time = 600 143 | #全局用户锁定时间(秒) 144 | global_user_lock_time = 300 145 | 146 | #全局IP单位时间内最大尝试次数,0为关闭该功能 147 | max_global_ip_ban_count = 40 148 | #全局IP设置单位时间(秒) 149 | global_ip_ban_reset_time = 1200 150 | #全局IP锁定时间(秒) 151 | global_ip_lock_time = 300 152 | 153 | #全局锁定状态的保存生命周期(秒),超过则删除记录 154 | global_lock_state_expiration_time = 3600 155 | -------------------------------------------------------------------------------- /server/conf/server.toml: -------------------------------------------------------------------------------- 1 | #示例配置信息 2 | 3 | #其他配置文件,可以使用绝对路径 4 | #或者相对于 anylink 二进制文件的路径 5 | 6 | #数据文件 7 | db_type = "sqlite3" 8 | db_source = "./conf/anylink.db" 9 | #证书文件 10 | cert_file = "./conf/vpn_cert.pem" 11 | cert_key = "./conf/vpn_cert.key" 12 | files_path = "./conf/files" 13 | 14 | #日志目录,默认为空写入标准输出 15 | #log_path = "./log" 16 | log_level = "debug" 17 | 18 | #系统名称 19 | issuer = "XX公司VPN" 20 | #后台管理用户 21 | admin_user = "admin" 22 | #pass 123456 23 | admin_pass = "$2a$10$UQ7C.EoPifDeJh6d8.31TeSPQU7hM/NOM2nixmBucJpAuXDQNqNke" 24 | # 留空表示不开启 otp, 开启otp后密码为 pass + 6位otp 25 | # 生成 ./anylink tool -o 26 | admin_otp = "" 27 | jwt_secret = "abcdef.0123456789.abcdef" 28 | 29 | #TCP服务监听地址(任意端口) 30 | server_addr = ":443" 31 | #开启 DTLS 32 | server_dtls = false 33 | #UDP监听地址(任意端口) 34 | server_dtls_addr = ":443" 35 | #后台服务监听地址 36 | admin_addr = ":8800" 37 | 38 | #最大客户端数量 39 | max_client = 200 40 | #单个用户同时在线数量 41 | max_user_client = 3 42 | 43 | #虚拟网络类型[tun macvtap] 44 | link_mode = "tun" 45 | #客户端分配的ip地址池 46 | #docker环境一般默认 eth0,其他情况根据实际网卡信息填写 47 | ipv4_master = "eth0" 48 | ipv4_cidr = "192.168.90.0/24" 49 | ipv4_gateway = "192.168.90.1" 50 | ipv4_start = "192.168.90.100" 51 | ipv4_end = "192.168.90.200" 52 | 53 | #是否自动添加nat 54 | iptables_nat = true 55 | 56 | #客户端显示详细错误信息(线上环境慎开启) 57 | display_error = true 58 | -------------------------------------------------------------------------------- /server/conf/vpn_cert.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDRVjwa+YPKcvKD 3 | 2YZJUMPcwXCL32hF9gFTyMFuojpvo/L9C710xkJBNW2ceEcAdYyohBhtylbCFKUy 4 | QpDRJsAmRiWL5zSjfeds4bVEInmXvF+Ga9FuN/o/R27/kyrejBVd7u9AKzg9k5vI 5 | /4FhXFMivZgwghaSiJkVHJG6ehmCVGR11A5LUIjrub+iYJfaS2dl4WVxn9kyYRuO 6 | NU0nDMInWE6F+dVa57HEUEOolDEmr9R+N5XIXYfXwE4W4Xj/5CL+mBLME+OaADK8 7 | RE9dPomgk3Vqc/s8thllyztqR/TfQIwCwmQNf+UB5DIWr5iYDsX4rU3hswJQUCTQ 8 | CLNEcdStAgMBAAECgf97UHp7u8+x4lz6BQB8/c0xHWdAw1clIveZbEVl0hNoVUN/ 9 | EKku+q/1Sf+CuhnbgLjT1bkVnnotmVcGeWH2DeCnkMJqHVMOBRb/dso2fw+pCTzY 10 | VofQpmbPy1xCXb6chqrpT3MTaJdI7IrBCNEQ54cOxsojwzp0MfejS/NI41q8iuCS 11 | hVry+VEbmtA22vjwMvsF6NsRKlfc1DUyC/ZoHB91gzKmvLdXBREwBXtdiJfMlu9G 12 | EKH3ORxbcanmdsXWIG84t5M9Sf9L5FZe6tlL4FdoKv25Vxv2z+PVM8Y/0B3IBTCb 13 | yqMK8579PTlHl40WRjSgzpijGvMxuIpgm5KaXQECgYEA7U6lBhRWmPsJRaK0jyAv 14 | qnaC5AbnM8OjIbsDtnQxn0BWvx7aZznxOE4jL0QejUD/gov6yYkNhIu797LzO8Mq 15 | fEqEX7Bp23PfLvybqkyhJWFz26v0erU5q8Rlt648bK7Ul1j8FkmsEN3qu+64c8Fu 16 | pzN081bqOGjEcaCjsHswvkMCgYEA4dOPvYYu8wBfrH3Gn94XHWWVvFMnWErXEOzK 17 | eL0dFg3Ae+y8Sg5x9YTz3XjwyzwcTbrD0zg4huWwmD64BgPcliG7XKA093ea2JIs 18 | Mah5/KTQmltcIut7qUvzwwbQVfN3xTwWzd7eDtEIAeFZ8r/HzJM6X21b/LaQIXUY 19 | Gb7dik8CgYAaaUxYlt7ke9wWUfuCinSDplj/A/2rdzSqxmOtZNU5AjIlZ0urfXlp 20 | aNjlo9E6q2dEokuxLn3AqMSs1s/XcOtDlg+RjtLZR9YpJpg0pf6xaF06r7KwDYdz 21 | pJIllVDIT9T9WzwDRwPNhMVhUTpaN8cW+NUlWCENUiu68cQGGk/cfQKBgEmvRk+I 22 | 4PjZPl6CC7VOOiyVYO46E7RzdwlGuin7SupPQmctL6LaY8TAxPGW7LrjujiCoDLj 23 | PU6G08BZdqI/0FIMX54xiBbXJ+dSiqkJWARfotE6zi12uLrc1YTlTEU/U+0/VhGG 24 | jt42xm4WocrbWM4fnARXIpSq3QyNsHd2F8NxAoGBAMEcT0mFQ0J7HCQ+zL36ogGv 25 | Bc/N6WR0xSEBQVG9D4iysxX4XyJukCuUCvMIbsBj5IVAVBlMNxfDtL/kD6tgy0L7 26 | tdN7nOaYgnqTyZfQItz92cQ6oiIqW7GPJA5ATJe1fxXINjcP6EkRDAfACBW7/l85 27 | Cd/yRpaOSMPbqKO1OOSn 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /server/cron/clear_audit.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "github.com/bjdgyc/anylink/base" 5 | "github.com/bjdgyc/anylink/dbdata" 6 | ) 7 | 8 | // 清除访问审计日志 9 | func ClearAudit() { 10 | lifeDay, timesUp := isClearTime() 11 | if !timesUp { 12 | return 13 | } 14 | // 当审计日志永久保存,则退出 15 | if lifeDay <= 0 { 16 | return 17 | } 18 | affected, err := dbdata.ClearAccessAudit(getTimeAgo(lifeDay)) 19 | base.Info("Cron ClearAudit: ", affected, err) 20 | } 21 | -------------------------------------------------------------------------------- /server/cron/clear_statsinfo.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/bjdgyc/anylink/base" 7 | "github.com/bjdgyc/anylink/dbdata" 8 | ) 9 | 10 | const siLifeDay = 30 11 | 12 | // 清除图表数据 13 | func ClearStatsInfo() { 14 | _, timesUp := isClearTime() 15 | if !timesUp { 16 | return 17 | } 18 | ts := getTimeAgo(siLifeDay) 19 | for _, item := range dbdata.StatsInfoIns.Actions { 20 | affected, err := dbdata.StatsInfoIns.ClearStatsInfo(item, ts) 21 | base.Info("Cron ClearStatsInfo "+item+": ", affected, err) 22 | } 23 | } 24 | 25 | // 是否到了"清理时间" 26 | func isClearTime() (int, bool) { 27 | dataLog, err := dbdata.SettingGetAuditLog() 28 | if err != nil { 29 | base.Error("Cron SettingGetLog: ", err) 30 | return -1, false 31 | } 32 | currentTime := time.Now().Format("15:04") 33 | // 未到"清理时间"时, 则返回 34 | if dataLog.ClearTime != currentTime { 35 | return -1, false 36 | } 37 | return dataLog.LifeDay, true 38 | } 39 | 40 | // 根据存储时长,获取清理日期 41 | func getTimeAgo(days int) string { 42 | var timeS string 43 | ts := time.Now().AddDate(0, 0, -days) 44 | tsZero := time.Date(ts.Year(), ts.Month(), ts.Day(), 0, 0, 0, 0, time.Local) 45 | timeS = tsZero.Format(dbdata.LayoutTimeFormat) 46 | return timeS 47 | } 48 | -------------------------------------------------------------------------------- /server/cron/clear_user_act_log.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "github.com/bjdgyc/anylink/base" 5 | "github.com/bjdgyc/anylink/dbdata" 6 | ) 7 | 8 | // 清除用户活动日志 9 | func ClearUserActLog() { 10 | lifeDay, timesUp := isClearTime() 11 | if !timesUp { 12 | return 13 | } 14 | // 当审计日志永久保存时,则退出 15 | if lifeDay <= 0 { 16 | return 17 | } 18 | affected, err := dbdata.UserActLogIns.ClearUserActLog(getTimeAgo(lifeDay)) 19 | base.Info("Cron ClearUserActLog: ", affected, err) 20 | } 21 | -------------------------------------------------------------------------------- /server/cron/start.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/bjdgyc/anylink/dbdata" 7 | "github.com/bjdgyc/anylink/sessdata" 8 | "github.com/go-co-op/gocron" 9 | ) 10 | 11 | func Start() { 12 | s := gocron.NewScheduler(time.Local) 13 | s.Cron("0 * * * *").Do(ClearAudit) 14 | s.Cron("0 * * * *").Do(ClearStatsInfo) 15 | s.Cron("0 * * * *").Do(ClearUserActLog) 16 | s.Every(1).Day().At("00:00").Do(sessdata.CloseUserLimittimeSession) 17 | s.Every(1).Day().At("00:00").Do(dbdata.ReNewCert) 18 | s.StartAsync() 19 | } 20 | -------------------------------------------------------------------------------- /server/dbdata/audit.go: -------------------------------------------------------------------------------- 1 | package dbdata 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "xorm.io/xorm" 7 | ) 8 | 9 | type SearchCon struct { 10 | Username string `json:"username"` 11 | Src string `json:"src"` 12 | Dst string `json:"dst"` 13 | DstPort string `json:"dst_port"` 14 | AccessProto string `json:"access_proto"` 15 | Date []string `json:"date"` 16 | Info string `json:"info"` 17 | Sort int `json:"sort"` 18 | } 19 | 20 | func GetAuditSession(search string) *xorm.Session { 21 | session := xdb.Where("1=1") 22 | if search == "" { 23 | return session 24 | } 25 | var searchData SearchCon 26 | err := json.Unmarshal([]byte(search), &searchData) 27 | if err != nil { 28 | return session 29 | } 30 | if searchData.Username != "" { 31 | session.And("username = ?", searchData.Username) 32 | } 33 | if searchData.Src != "" { 34 | session.And("src = ?", searchData.Src) 35 | } 36 | if searchData.Dst != "" { 37 | session.And("dst = ?", searchData.Dst) 38 | } 39 | if searchData.DstPort != "" { 40 | session.And("dst_port = ?", searchData.DstPort) 41 | } 42 | if searchData.AccessProto != "" { 43 | session.And("access_proto = ?", searchData.AccessProto) 44 | } 45 | if len(searchData.Date) > 0 && searchData.Date[0] != "" { 46 | session.And("created_at BETWEEN ? AND ?", searchData.Date[0], searchData.Date[1]) 47 | } 48 | if searchData.Info != "" { 49 | session.And("info LIKE ?", "%"+searchData.Info+"%") 50 | } 51 | if searchData.Sort == 1 { 52 | session.OrderBy("id desc") 53 | } else { 54 | session.OrderBy("id asc") 55 | } 56 | return session 57 | } 58 | 59 | func ClearAccessAudit(ts string) (int64, error) { 60 | affected, err := xdb.Where("created_at < '" + ts + "'").Delete(&AccessAudit{}) 61 | return affected, err 62 | } 63 | -------------------------------------------------------------------------------- /server/dbdata/audit_test.go: -------------------------------------------------------------------------------- 1 | package dbdata 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestSearchAudit(t *testing.T) { 12 | ast := assert.New(t) 13 | 14 | preIpData() 15 | defer closeIpdata() 16 | 17 | currDateVal := "2022-07-24 00:00:00" 18 | CreatedAt, _ := time.ParseInLocation("2006-01-02 15:04:05", currDateVal, time.Local) 19 | 20 | dataTest := AccessAudit{ 21 | Username: "Test", 22 | Protocol: 6, 23 | Src: "10.10.1.5", 24 | SrcPort: 0, 25 | Dst: "172.217.160.68", 26 | DstPort: 80, 27 | AccessProto: 4, 28 | Info: "www.google.com", 29 | CreatedAt: CreatedAt, 30 | } 31 | err := Add(dataTest) 32 | ast.Nil(err) 33 | 34 | var datas []AccessAudit 35 | searchFormat := `{"username": "%s", "src":"%s", "dst": "%s", "dst_port":"%d","access_proto":"%d","info":"%s","date":["%s","%s"]}` 36 | search := fmt.Sprintf(searchFormat, dataTest.Username, dataTest.Src, dataTest.Dst, dataTest.DstPort, dataTest.AccessProto, dataTest.Info, currDateVal, currDateVal) 37 | 38 | session := GetAuditSession(search) 39 | count, _ := FindAndCount(session, &datas, PageSize, 0) 40 | ast.Equal(count, int64(1)) 41 | ast.Equal(datas[0].Username, dataTest.Username) 42 | ast.Equal(datas[0].Dst, dataTest.Dst) 43 | } 44 | -------------------------------------------------------------------------------- /server/dbdata/db_orm.go: -------------------------------------------------------------------------------- 1 | package dbdata 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | 7 | "xorm.io/xorm" 8 | ) 9 | 10 | const PageSize = 10 11 | 12 | var ErrNotFound = errors.New("ErrNotFound") 13 | 14 | func Add(data interface{}) error { 15 | _, err := xdb.InsertOne(data) 16 | return err 17 | } 18 | 19 | func AddBatch(data interface{}) error { 20 | _, err := xdb.Insert(data) 21 | return err 22 | } 23 | 24 | func Update(fieldName string, value interface{}, data interface{}) error { 25 | _, err := xdb.Where(fieldName+"=?", value).Update(data) 26 | return err 27 | } 28 | 29 | func Del(data interface{}) error { 30 | _, err := xdb.Delete(data) 31 | return err 32 | } 33 | 34 | func extract(data interface{}, fieldName string) interface{} { 35 | ref := reflect.ValueOf(data) 36 | r := &ref 37 | if r.Kind() == reflect.Ptr { 38 | e := r.Elem() 39 | r = &e 40 | } 41 | field := r.FieldByName(fieldName).Interface() 42 | return field 43 | } 44 | 45 | // 更新全部字段 46 | func Set(data interface{}) error { 47 | id := extract(data, "Id") 48 | _, err := xdb.ID(id).AllCols().Update(data) 49 | return err 50 | } 51 | 52 | func One(fieldName string, value interface{}, data interface{}) error { 53 | has, err := xdb.Where(fieldName+"=?", value).Get(data) 54 | if err != nil { 55 | return err 56 | } 57 | if !has { 58 | return ErrNotFound 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func CountAll(data interface{}) int { 65 | n, _ := xdb.Count(data) 66 | return int(n) 67 | } 68 | 69 | func Find(data interface{}, limit, page int) error { 70 | if limit == 0 { 71 | return xdb.Find(data) 72 | } 73 | 74 | start := (page - 1) * limit 75 | return xdb.Limit(limit, start).Find(data) 76 | } 77 | 78 | func FindWhereCount(data interface{}, where string, args ...interface{}) int { 79 | n, _ := xdb.Where(where, args...).Count(data) 80 | return int(n) 81 | } 82 | 83 | func FindWhere(data interface{}, limit int, page int, where string, args ...interface{}) error { 84 | if limit == 0 { 85 | return xdb.Where(where, args...).Find(data) 86 | } 87 | 88 | start := (page - 1) * limit 89 | return xdb.Where(where, args...).Limit(limit, start).Find(data) 90 | } 91 | 92 | func CountPrefix(fieldName string, prefix string, data interface{}) int { 93 | n, _ := xdb.Where(fieldName+" like ?", prefix+"%").Count(data) 94 | return int(n) 95 | } 96 | 97 | func Prefix(fieldName string, prefix string, data interface{}, limit, page int) error { 98 | where := xdb.Where(fieldName+" like ?", prefix+"%") 99 | if limit == 0 { 100 | return where.Find(data) 101 | } 102 | 103 | start := (page - 1) * limit 104 | return where.Limit(limit, start).Find(data) 105 | } 106 | 107 | func FindAndCount(session *xorm.Session, data interface{}, limit, page int) (int64, error) { 108 | if limit == 0 { 109 | return session.FindAndCount(data) 110 | } 111 | start := (page - 1) * limit 112 | totalCount, err := session.Limit(limit, start).FindAndCount(data) 113 | return totalCount, err 114 | } 115 | -------------------------------------------------------------------------------- /server/dbdata/db_test.go: -------------------------------------------------------------------------------- 1 | package dbdata 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | 8 | "github.com/bjdgyc/anylink/base" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func preIpData() { 13 | tmpDb := path.Join(os.TempDir(), "anylink_test.db") 14 | base.Cfg.DbType = "sqlite3" 15 | base.Cfg.DbSource = tmpDb 16 | initDb() 17 | } 18 | 19 | func closeIpdata() { 20 | xdb.Close() 21 | tmpDb := path.Join(os.TempDir(), "anylink_test.db") 22 | os.Remove(tmpDb) 23 | } 24 | 25 | func TestDb(t *testing.T) { 26 | ast := assert.New(t) 27 | preIpData() 28 | defer closeIpdata() 29 | 30 | u := User{Username: "a"} 31 | err := Add(&u) 32 | ast.Nil(err) 33 | 34 | ast.Equal(u.Id, 1) 35 | } 36 | -------------------------------------------------------------------------------- /server/dbdata/group_test.go: -------------------------------------------------------------------------------- 1 | package dbdata 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/bjdgyc/anylink/pkg/utils" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGetGroupNames(t *testing.T) { 11 | ast := assert.New(t) 12 | 13 | preIpData() 14 | defer closeIpdata() 15 | 16 | // 添加 group 17 | g1 := Group{Name: "g1", ClientDns: []ValData{{Val: "114.114.114.114"}}} 18 | err := SetGroup(&g1) 19 | ast.Nil(err) 20 | g2 := Group{Name: "g2", ClientDns: []ValData{{Val: "114.114.114.114"}}} 21 | err = SetGroup(&g2) 22 | ast.Nil(err) 23 | g3 := Group{Name: "g3", ClientDns: []ValData{{Val: "114.114.114.114"}}} 24 | err = SetGroup(&g3) 25 | ast.Nil(err) 26 | 27 | authData := map[string]interface{}{ 28 | "type": "radius", 29 | "radius": map[string]string{ 30 | "addr": "192.168.8.12:1044", 31 | "secret": "43214132", 32 | }, 33 | } 34 | g4 := Group{Name: "g4", ClientDns: []ValData{{Val: "114.114.114.114"}}, Auth: authData} 35 | err = SetGroup(&g4) 36 | ast.Nil(err) 37 | g5 := Group{Name: "g5", ClientDns: []ValData{{Val: "114.114.114.114"}}, DsIncludeDomains: "baidu.com,163.com"} 38 | err = SetGroup(&g5) 39 | if ast.NotNil(err) { 40 | ast.Equal("默认路由, 不允许设置\"包含域名\", 请重新配置", err.Error()) 41 | } 42 | g6 := Group{Name: "g6", ClientDns: []ValData{{Val: "114.114.114.114"}}, DsExcludeDomains: "com.cn,qq.com"} 43 | err = SetGroup(&g6) 44 | ast.Nil(err) 45 | 46 | authData = map[string]interface{}{ 47 | "type": "ldap", 48 | "ldap": map[string]interface{}{ 49 | "addr": "192.168.8.12:389", 50 | "tls": true, 51 | "bind_name": "userfind@abc.com", 52 | "bind_pwd": "afdbfdsafds", 53 | "base_dn": "dc=abc,dc=com", 54 | "object_class": "person", 55 | "search_attr": "sAMAccountName", 56 | "member_of": "cn=vpn,cn=user,dc=abc,dc=com", 57 | }, 58 | } 59 | g7 := Group{Name: "g7", ClientDns: []ValData{{Val: "114.114.114.114"}}, Auth: authData} 60 | err = SetGroup(&g7) 61 | ast.Nil(err) 62 | 63 | // 判断所有数据 64 | gAll := []string{"g1", "g2", "g3", "g4", "g5", "g6", "g7"} 65 | gs := GetGroupNames() 66 | for _, v := range gs { 67 | ast.Equal(true, utils.InArrStr(gAll, v)) 68 | } 69 | 70 | gni := GetGroupNamesIds() 71 | for _, v := range gni { 72 | ast.NotEqual(0, v.Id) 73 | ast.Equal(true, utils.InArrStr(gAll, v.Name)) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /server/dbdata/ip_map.go: -------------------------------------------------------------------------------- 1 | package dbdata 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "time" 7 | ) 8 | 9 | type IpMap struct { 10 | Id int `json:"id" xorm:"pk autoincr not null"` 11 | IpAddr string `json:"ip_addr" xorm:"varchar(32) not null unique"` 12 | MacAddr string `json:"mac_addr" xorm:"varchar(32) not null unique"` 13 | UniqueMac bool `json:"unique_mac" xorm:"Bool index"` 14 | Username string `json:"username" xorm:"varchar(60)"` 15 | Keep bool `json:"keep" xorm:"Bool"` // 保留 ip-mac 绑定 16 | KeepTime time.Time `json:"keep_time" xorm:"DateTime"` 17 | Note string `json:"note" xorm:"varchar(255)"` // 备注 18 | LastLogin time.Time `json:"last_login" xorm:"DateTime"` 19 | UpdatedAt time.Time `json:"updated_at" xorm:"DateTime updated"` 20 | } 21 | 22 | func SetIpMap(v *IpMap) error { 23 | var err error 24 | 25 | if len(v.IpAddr) < 4 || len(v.MacAddr) < 6 { 26 | return errors.New("IP或MAC错误") 27 | } 28 | 29 | macHw, err := net.ParseMAC(v.MacAddr) 30 | if err != nil { 31 | return errors.New("MAC错误") 32 | } 33 | // 统一macAddr的格式 34 | v.MacAddr = macHw.String() 35 | 36 | v.UpdatedAt = time.Now() 37 | if v.Id > 0 { 38 | err = Set(v) 39 | } else { 40 | err = Add(v) 41 | } 42 | return err 43 | } 44 | -------------------------------------------------------------------------------- /server/dbdata/policy.go: -------------------------------------------------------------------------------- 1 | package dbdata 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | func GetPolicy(Username string) *Policy { 12 | policyData := &Policy{} 13 | err := One("Username", Username, policyData) 14 | if err != nil { 15 | return policyData 16 | } 17 | return policyData 18 | } 19 | 20 | func SetPolicy(p *Policy) error { 21 | var err error 22 | if p.Username == "" { 23 | return errors.New("用户名错误") 24 | } 25 | 26 | // 包含路由 27 | routeInclude := []ValData{} 28 | for _, v := range p.RouteInclude { 29 | if v.Val != "" { 30 | if v.Val == ALL { 31 | routeInclude = append(routeInclude, v) 32 | continue 33 | } 34 | 35 | ipMask, ipNet, err := parseIpNet(v.Val) 36 | if err != nil { 37 | return errors.New("RouteInclude 错误" + err.Error()) 38 | } 39 | 40 | if strings.Split(ipMask, "/")[0] != ipNet.IP.String() { 41 | errMsg := fmt.Sprintf("RouteInclude 错误: 网络地址错误,建议: %s 改为 %s", v.Val, ipNet) 42 | return errors.New(errMsg) 43 | } 44 | 45 | v.IpMask = ipMask 46 | routeInclude = append(routeInclude, v) 47 | } 48 | } 49 | p.RouteInclude = routeInclude 50 | // 排除路由 51 | routeExclude := []ValData{} 52 | for _, v := range p.RouteExclude { 53 | if v.Val != "" { 54 | ipMask, ipNet, err := parseIpNet(v.Val) 55 | if err != nil { 56 | return errors.New("RouteExclude 错误" + err.Error()) 57 | } 58 | 59 | if strings.Split(ipMask, "/")[0] != ipNet.IP.String() { 60 | errMsg := fmt.Sprintf("RouteInclude 错误: 网络地址错误,建议: %s 改为 %s", v.Val, ipNet) 61 | return errors.New(errMsg) 62 | } 63 | v.IpMask = ipMask 64 | routeExclude = append(routeExclude, v) 65 | } 66 | } 67 | p.RouteExclude = routeExclude 68 | 69 | // DNS 判断 70 | clientDns := []ValData{} 71 | for _, v := range p.ClientDns { 72 | if v.Val != "" { 73 | ip := net.ParseIP(v.Val) 74 | if ip.String() != v.Val { 75 | return errors.New("DNS IP 错误") 76 | } 77 | clientDns = append(clientDns, v) 78 | } 79 | } 80 | if len(routeInclude) == 0 || (len(routeInclude) == 1 && routeInclude[0].Val == "all") { 81 | if len(clientDns) == 0 { 82 | return errors.New("默认路由,必须设置一个DNS") 83 | } 84 | } 85 | p.ClientDns = clientDns 86 | 87 | // 域名拆分隧道,不能同时填写 88 | p.DsIncludeDomains = strings.TrimSpace(p.DsIncludeDomains) 89 | p.DsExcludeDomains = strings.TrimSpace(p.DsExcludeDomains) 90 | if p.DsIncludeDomains != "" && p.DsExcludeDomains != "" { 91 | return errors.New("包含/排除域名不能同时填写") 92 | } 93 | // 校验包含域名的格式 94 | err = CheckDomainNames(p.DsIncludeDomains) 95 | if err != nil { 96 | return errors.New("包含域名有误:" + err.Error()) 97 | } 98 | // 校验排除域名的格式 99 | err = CheckDomainNames(p.DsExcludeDomains) 100 | if err != nil { 101 | return errors.New("排除域名有误:" + err.Error()) 102 | } 103 | 104 | p.UpdatedAt = time.Now() 105 | if p.Id > 0 { 106 | err = Set(p) 107 | } else { 108 | err = Add(p) 109 | } 110 | 111 | return err 112 | } 113 | -------------------------------------------------------------------------------- /server/dbdata/policy_test.go: -------------------------------------------------------------------------------- 1 | package dbdata 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGetPolicy(t *testing.T) { 10 | ast := assert.New(t) 11 | 12 | preIpData() 13 | defer closeIpdata() 14 | 15 | // 添加 Policy 16 | p1 := Policy{Username: "a1", ClientDns: []ValData{{Val: "114.114.114.114"}}, DsExcludeDomains: "baidu.com,163.com"} 17 | err := SetPolicy(&p1) 18 | ast.Nil(err) 19 | 20 | p2 := Policy{Username: "a2", ClientDns: []ValData{{Val: "114.114.114.114"}}, DsExcludeDomains: "com.cn,qq.com"} 21 | err = SetPolicy(&p2) 22 | ast.Nil(err) 23 | 24 | route := []ValData{{Val: "192.168.1.0/24"}} 25 | p3 := Policy{Username: "a3", ClientDns: []ValData{{Val: "114.114.114.114"}}, RouteInclude: route, DsExcludeDomains: "com.cn,qq.com"} 26 | err = SetPolicy(&p3) 27 | ast.Nil(err) 28 | // 判断 IpMask 29 | ast.Equal(p3.RouteInclude[0].IpMask, "192.168.1.0/255.255.255.0") 30 | 31 | route2 := []ValData{{Val: "192.168.2.0/24"}} 32 | p4 := Policy{Username: "a4", ClientDns: []ValData{{Val: "114.114.114.114"}}, RouteExclude: route2, DsIncludeDomains: "com.cn,qq.com"} 33 | err = SetPolicy(&p4) 34 | ast.Nil(err) 35 | // 判断 IpMask 36 | ast.Equal(p4.RouteExclude[0].IpMask, "192.168.2.0/255.255.255.0") 37 | 38 | // 判断所有数据 39 | var userPolicy *Policy 40 | pAll := []string{"a1", "a2", "a3", "a4"} 41 | for _, v := range pAll { 42 | userPolicy = GetPolicy(v) 43 | ast.NotEqual(userPolicy.Id, 0, "user policy id is zero") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /server/dbdata/setting.go: -------------------------------------------------------------------------------- 1 | package dbdata 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | 7 | "xorm.io/xorm" 8 | ) 9 | 10 | type SettingInstall struct { 11 | Installed bool `json:"installed"` 12 | } 13 | 14 | type SettingSmtp struct { 15 | Host string `json:"host"` 16 | Port int `json:"port"` 17 | Username string `json:"username"` 18 | Password string `json:"password"` 19 | From string `json:"from"` 20 | Encryption string `json:"encryption"` 21 | } 22 | 23 | type SettingAuditLog struct { 24 | AuditInterval int `json:"audit_interval"` 25 | LifeDay int `json:"life_day"` 26 | ClearTime string `json:"clear_time"` 27 | } 28 | 29 | type SettingOther struct { 30 | LinkAddr string `json:"link_addr"` 31 | Banner string `json:"banner"` 32 | Homecode int `json:"homecode"` 33 | Homeindex string `json:"homeindex"` 34 | AccountMail string `json:"account_mail"` 35 | } 36 | 37 | func StructName(data interface{}) string { 38 | ref := reflect.ValueOf(data) 39 | s := &ref 40 | if s.Kind() == reflect.Ptr { 41 | e := s.Elem() 42 | s = &e 43 | } 44 | name := s.Type().Name() 45 | return name 46 | } 47 | 48 | func SettingSessAdd(sess *xorm.Session, data interface{}) error { 49 | name := StructName(data) 50 | v, _ := json.Marshal(data) 51 | s := &Setting{Name: name, Data: v} 52 | _, err := sess.InsertOne(s) 53 | return err 54 | } 55 | 56 | func SettingSet(data interface{}) error { 57 | name := StructName(data) 58 | v, _ := json.Marshal(data) 59 | s := &Setting{Data: v} 60 | err := Update("name", name, s) 61 | return err 62 | } 63 | 64 | func SettingGet(data interface{}) error { 65 | name := StructName(data) 66 | s := &Setting{} 67 | err := One("name", name, s) 68 | if err != nil { 69 | return err 70 | } 71 | err = json.Unmarshal(s.Data, data) 72 | return err 73 | } 74 | 75 | func SettingGetAuditLog() (SettingAuditLog, error) { 76 | data := SettingAuditLog{} 77 | err := SettingGet(&data) 78 | if err == nil { 79 | return data, err 80 | } 81 | if !CheckErrNotFound(err) { 82 | return data, err 83 | } 84 | sess := xdb.NewSession() 85 | defer sess.Close() 86 | auditLog := SettingGetAuditLogDefault() 87 | err = SettingSessAdd(sess, auditLog) 88 | if err != nil { 89 | return data, err 90 | } 91 | return auditLog, nil 92 | } 93 | 94 | func SettingGetAuditLogDefault() SettingAuditLog { 95 | auditLog := SettingAuditLog{ 96 | LifeDay: 0, 97 | ClearTime: "05:00", 98 | } 99 | return auditLog 100 | } 101 | -------------------------------------------------------------------------------- /server/dbdata/start.go: -------------------------------------------------------------------------------- 1 | package dbdata 2 | 3 | func Start() { 4 | initDb() 5 | initData() 6 | } 7 | 8 | func Stop() error { 9 | return xdb.Close() 10 | } 11 | -------------------------------------------------------------------------------- /server/dbdata/statsinfo_test.go: -------------------------------------------------------------------------------- 1 | package dbdata 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestStatsInfo(t *testing.T) { 11 | ast := assert.New(t) 12 | 13 | preIpData() 14 | defer closeIpdata() 15 | 16 | ast.True(StatsInfoIns.ValidAction("online")) 17 | ast.False(StatsInfoIns.ValidAction("diskio")) 18 | ast.True(StatsInfoIns.ValidScope("30d")) 19 | ast.False(StatsInfoIns.ValidScope("60d")) 20 | 21 | up := uint32(100) 22 | down := uint32(300) 23 | upGroups := map[int]uint32{1: up} 24 | downGroups := map[int]uint32{1: down} 25 | numGroups := map[int]int{1: 5} 26 | // online 27 | numData, _ := json.Marshal(numGroups) 28 | so := StatsOnline{Num: 1, NumGroups: string(numData)} 29 | // network 30 | upData, _ := json.Marshal(upGroups) 31 | downData, _ := json.Marshal(downGroups) 32 | sn := StatsNetwork{Up: up, Down: down, UpGroups: string(upData), DownGroups: string(downData)} 33 | // cpu 34 | sc := StatsCpu{Percent: 0.3} 35 | // mem 36 | sm := StatsMem{Percent: 24.50} 37 | 38 | StatsInfoIns.SetRealTime("online", so) 39 | StatsInfoIns.GetRealTime("online") 40 | StatsInfoIns.SaveStatsInfo(so, sn, sc, sm) 41 | 42 | var err error 43 | _, err = StatsInfoIns.GetData("online", "1h") 44 | ast.Nil(err) 45 | 46 | _, err = StatsInfoIns.GetData("network", "1h") 47 | ast.Nil(err) 48 | 49 | _, err = StatsInfoIns.GetData("cpu", "1h") 50 | ast.Nil(err) 51 | 52 | _, err = StatsInfoIns.GetData("mem", "1h") 53 | ast.Nil(err) 54 | 55 | } 56 | -------------------------------------------------------------------------------- /server/dbdata/user_act_log_test.go: -------------------------------------------------------------------------------- 1 | package dbdata 2 | 3 | import "testing" 4 | 5 | func TestParseUserAgent(t *testing.T) { 6 | type args struct { 7 | userAgent string 8 | } 9 | type res struct { 10 | os_idx uint8 11 | client_idx uint8 12 | ver string 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want res 18 | }{ 19 | { 20 | name: "mac os 1", 21 | args: args{userAgent: "cisco anyconnect vpn agent for mac os x 4.10.05085"}, 22 | want: res{os_idx: 2, client_idx: 1, ver: "4.10.05085"}, 23 | }, 24 | { 25 | name: "mac os 2", 26 | args: args{userAgent: "anyconnect darwin_i386 4.10.05085"}, 27 | want: res{os_idx: 2, client_idx: 1, ver: "4.10.05085"}, 28 | }, 29 | { 30 | name: "windows", 31 | args: args{userAgent: "cisco anyconnect vpn agent for windows 4.8.02042"}, 32 | want: res{os_idx: 1, client_idx: 1, ver: "4.8.02042"}, 33 | }, 34 | { 35 | name: "iPad", 36 | args: args{userAgent: "anyconnect applesslvpn_darwin_arm (ipad) 4.10.04060"}, 37 | want: res{os_idx: 5, client_idx: 1, ver: "4.10.04060"}, 38 | }, 39 | { 40 | name: "iPhone", 41 | args: args{userAgent: "cisco anyconnect vpn agent for apple iphone 4.10.04060"}, 42 | want: res{os_idx: 5, client_idx: 1, ver: "4.10.04060"}, 43 | }, 44 | { 45 | name: "android", 46 | args: args{userAgent: "anyconnect android 4.10.05096"}, 47 | want: res{os_idx: 4, client_idx: 1, ver: "4.10.05096"}, 48 | }, 49 | { 50 | name: "linux", 51 | args: args{userAgent: "cisco anyconnect vpn agent for linux v7.08"}, 52 | want: res{os_idx: 3, client_idx: 1, ver: "7.08"}, 53 | }, 54 | { 55 | name: "openconnect", 56 | args: args{userAgent: "openconnect-gui 1.5.3 v7.08"}, 57 | want: res{os_idx: 0, client_idx: 2, ver: "7.08"}, 58 | }, 59 | { 60 | name: "unknown", 61 | args: args{userAgent: "unknown 1.4.3 aabcd"}, 62 | want: res{os_idx: 0, client_idx: 0, ver: ""}, 63 | }, 64 | { 65 | name: "unknown 2", 66 | args: args{userAgent: ""}, 67 | want: res{os_idx: 0, client_idx: 0, ver: ""}, 68 | }, 69 | { 70 | name: "anylink", 71 | args: args{userAgent: "anylink vpn agent for linux v1.0"}, 72 | want: res{os_idx: 3, client_idx: 3, ver: "1.0"}, 73 | }, 74 | } 75 | for _, tt := range tests { 76 | t.Run(tt.name, func(t *testing.T) { 77 | if os_idx, client_idx, ver := UserActLogIns.ParseUserAgent(tt.args.userAgent); os_idx != tt.want.os_idx || client_idx != tt.want.client_idx || ver != tt.want.ver { 78 | t.Errorf("ParseUserAgent() = %v, %v, %v, want %v, %v, %v", os_idx, client_idx, ver, tt.want.os_idx, tt.want.client_idx, tt.want.ver) 79 | } 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /server/dbdata/user_test.go: -------------------------------------------------------------------------------- 1 | package dbdata 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/bjdgyc/anylink/base" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestCheckUser(t *testing.T) { 11 | base.Test() 12 | ast := assert.New(t) 13 | 14 | preIpData() 15 | defer closeIpdata() 16 | 17 | group := "group1" 18 | 19 | // 添加一个组 20 | dns := []ValData{{Val: "114.114.114.114"}} 21 | route := []ValData{{Val: "192.168.1.0/24"}} 22 | g := Group{Name: group, Status: 1, ClientDns: dns, RouteInclude: route} 23 | err := SetGroup(&g) 24 | ast.Nil(err) 25 | // 判断 IpMask 26 | ast.Equal(g.RouteInclude[0].IpMask, "192.168.1.0/255.255.255.0") 27 | 28 | // 添加一个用户 29 | pincode := "a123456" 30 | u := User{Username: "aaa", PinCode: pincode, Groups: []string{group}, Status: 1} 31 | err = SetUser(&u) 32 | ast.Nil(err) 33 | 34 | // 验证 PinCode + OtpSecret 35 | // totp := gotp.NewDefaultTOTP(u.OtpSecret) 36 | // secret := totp.Now() 37 | // err = CheckUser("aaa", u.PinCode+secret, group) 38 | // ast.Nil(err) 39 | 40 | // 单独验证密码 41 | u.DisableOtp = true 42 | _ = SetUser(&u) 43 | ext := map[string]any{ 44 | "mac_addr": "", 45 | } 46 | err = CheckUser("aaa", pincode, group, ext) 47 | ast.Nil(err) 48 | 49 | // 添加一个radius组 50 | group2 := "group2" 51 | authData := map[string]interface{}{ 52 | "type": "radius", 53 | "radius": map[string]string{ 54 | "addr": "192.168.1.12:1044", 55 | "secret": "43214132", 56 | }, 57 | } 58 | g2 := Group{Name: group2, Status: 1, ClientDns: dns, RouteInclude: route, Auth: authData} 59 | err = SetGroup(&g2) 60 | ast.Nil(err) 61 | err = CheckUser("aaa", "bbbbbbb", group2, ext) 62 | if ast.NotNil(err) { 63 | ast.Contains(err.Error(), "aaa Radius服务器连接异常") 64 | } 65 | // 添加用户策略 66 | dns2 := []ValData{{Val: "8.8.8.8"}} 67 | route2 := []ValData{{Val: "192.168.2.0/24"}} 68 | p1 := Policy{Username: "aaa", Status: 1, ClientDns: dns2, RouteInclude: route2} 69 | err = SetPolicy(&p1) 70 | ast.Nil(err) 71 | err = CheckUser("aaa", pincode, group, ext) 72 | ast.Nil(err) 73 | // 添加一个ldap组 74 | group3 := "group3" 75 | authData = map[string]interface{}{ 76 | "type": "ldap", 77 | "ldap": map[string]interface{}{ 78 | "addr": "192.168.8.12:389", 79 | "tls": true, 80 | "bind_name": "userfind@abc.com", 81 | "bind_pwd": "afdbfdsafds", 82 | "base_dn": "dc=abc,dc=com", 83 | "object_class": "person", 84 | "search_attr": "sAMAccountName", 85 | "member_of": "cn=vpn,cn=user,dc=abc,dc=com", 86 | }, 87 | } 88 | g3 := Group{Name: group3, Status: 1, ClientDns: dns, RouteInclude: route, Auth: authData} 89 | err = SetGroup(&g3) 90 | ast.Nil(err) 91 | err = CheckUser("aaa", "bbbbbbb", group3, ext) 92 | if ast.NotNil(err) { 93 | ast.Equal("aaa LDAP服务器连接异常, 请检测服务器和端口", err.Error()) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /server/dbdata/userauth.go: -------------------------------------------------------------------------------- 1 | package dbdata 2 | 3 | import ( 4 | "reflect" 5 | "regexp" 6 | ) 7 | 8 | var authRegistry = make(map[string]reflect.Type) 9 | 10 | type IUserAuth interface { 11 | checkData(authData map[string]interface{}) error 12 | checkUser(name, pwd string, g *Group, ext map[string]interface{}) error 13 | } 14 | 15 | func makeInstance(name string) interface{} { 16 | v := reflect.New(authRegistry[name]).Elem() 17 | return v.Interface() 18 | } 19 | 20 | func ValidateIpPort(addr string) bool { 21 | RegExp := regexp.MustCompile(`^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\:([0-9]|[1-9]\d{1,3}|[1-5]\d{4}|6[0-5]{2}[0-3][0-5])$$`) 22 | return RegExp.MatchString(addr) 23 | } 24 | -------------------------------------------------------------------------------- /server/dbdata/userauth_radius.go: -------------------------------------------------------------------------------- 1 | package dbdata 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "net" 9 | "reflect" 10 | "time" 11 | 12 | "github.com/bjdgyc/anylink/base" 13 | "layeh.com/radius" 14 | "layeh.com/radius/rfc2865" 15 | ) 16 | 17 | type AuthRadius struct { 18 | Addr string `json:"addr"` 19 | Secret string `json:"secret"` 20 | Nasip string `json:"nasip"` 21 | } 22 | 23 | func init() { 24 | authRegistry["radius"] = reflect.TypeOf(AuthRadius{}) 25 | } 26 | 27 | func (auth AuthRadius) checkData(authData map[string]interface{}) error { 28 | authType := authData["type"].(string) 29 | bodyBytes, err := json.Marshal(authData[authType]) 30 | if err != nil { 31 | return errors.New("Radius的密钥/服务器地址填写有误") 32 | } 33 | json.Unmarshal(bodyBytes, &auth) 34 | if !ValidateIpPort(auth.Addr) { 35 | return errors.New("Radius的服务器地址填写有误") 36 | } 37 | // freeradius官网最大8000字符, 这里限制200 38 | if len(auth.Secret) < 8 || len(auth.Secret) > 200 { 39 | return errors.New("Radius的密钥长度需在8~200个字符之间") 40 | } 41 | return nil 42 | } 43 | 44 | func (auth AuthRadius) checkUser(name, pwd string, g *Group, ext map[string]interface{}) error { 45 | pl := len(pwd) 46 | if name == "" || pl < 1 { 47 | return fmt.Errorf("%s %s", name, "密码错误") 48 | } 49 | authType := g.Auth["type"].(string) 50 | if _, ok := g.Auth[authType]; !ok { 51 | return fmt.Errorf("%s %s", name, "Radius的radius值不存在") 52 | } 53 | bodyBytes, err := json.Marshal(g.Auth[authType]) 54 | if err != nil { 55 | return fmt.Errorf("%s %s", name, "Radius Marshal出现错误") 56 | } 57 | err = json.Unmarshal(bodyBytes, &auth) 58 | if err != nil { 59 | return fmt.Errorf("%s %s", name, "Radius Unmarshal出现错误") 60 | } 61 | // radius认证时,设置超时3秒 62 | packet := radius.New(radius.CodeAccessRequest, []byte(auth.Secret)) 63 | err = rfc2865.UserName_SetString(packet, name) 64 | if err != nil { 65 | return fmt.Errorf("%s %s", name, "Radius set name 出现错误") 66 | } 67 | err = rfc2865.UserPassword_SetString(packet, pwd) 68 | if err != nil { 69 | return fmt.Errorf("%s %s", name, "Radius set pwd 出现错误") 70 | } 71 | if auth.Nasip != "" { 72 | nasip := net.ParseIP(auth.Nasip) 73 | err = rfc2865.NASIPAddress_Set(packet, nasip) 74 | if err != nil { 75 | return fmt.Errorf("%s %s", name, "Radius set nasip 出现错误") 76 | } 77 | } 78 | macAddr := "" 79 | if ext["mac_addr"] != nil { 80 | macAddr = ext["mac_addr"].(string) 81 | base.Trace("AuthRadius", ext, macAddr) 82 | } 83 | if macAddr != "" { 84 | err = rfc2865.CallingStationID_AddString(packet, macAddr) 85 | if err != nil { 86 | return fmt.Errorf("%s %s", name, "Radius set CallingStationID 出现错误") 87 | } 88 | } 89 | 90 | ctx, done := context.WithTimeout(context.Background(), 3*time.Second) 91 | defer done() 92 | response, err := radius.Exchange(ctx, packet, auth.Addr) 93 | if err != nil { 94 | return fmt.Errorf("%s %s %s", name, "Radius服务器连接异常, 请检测服务器和端口", err) 95 | } 96 | if response.Code != radius.CodeAccessAccept { 97 | return fmt.Errorf("%s %s", name, "Radius:用户名或密码错误") 98 | } 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /server/handler/dtls.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/tls" 8 | "encoding/hex" 9 | "errors" 10 | "net" 11 | "strings" 12 | "time" 13 | 14 | "github.com/bjdgyc/anylink/base" 15 | "github.com/bjdgyc/anylink/sessdata" 16 | "github.com/pion/dtls/v2" 17 | "github.com/pion/dtls/v2/pkg/crypto/selfsign" 18 | "github.com/pion/logging" 19 | ) 20 | 21 | func startDtls() { 22 | if !base.Cfg.ServerDTLS { 23 | return 24 | } 25 | 26 | // rsa 兼容 open connect 27 | priv, _ := rsa.GenerateKey(rand.Reader, 2048) 28 | certificate, err := selfsign.SelfSign(priv) 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | logf := logging.NewDefaultLoggerFactory() 34 | logf.Writer = base.GetBaseLw() 35 | logf.DefaultLogLevel = logging.LogLevelInfo 36 | if base.GetLogLevel() == base.LogLevelTrace { 37 | // logf.DefaultLogLevel = logging.LogLevelTrace 38 | } 39 | 40 | // https://github.com/pion/dtls/pull/369 41 | sessStore := &sessionStore{} 42 | 43 | config := &dtls.Config{ 44 | Certificates: []tls.Certificate{certificate}, 45 | ExtendedMasterSecret: dtls.DisableExtendedMasterSecret, 46 | CipherSuites: func() []dtls.CipherSuiteID { 47 | var cs = []dtls.CipherSuiteID{} 48 | for _, vv := range dtlsCipherSuites { 49 | cs = append(cs, vv) 50 | } 51 | return cs 52 | }(), 53 | LoggerFactory: logf, 54 | MTU: BufferSize, 55 | SessionStore: sessStore, 56 | ConnectContextMaker: func() (context.Context, func()) { 57 | return context.WithTimeout(context.Background(), 5*time.Second) 58 | }, 59 | } 60 | 61 | addr, err := net.ResolveUDPAddr("udp", base.Cfg.ServerDTLSAddr) 62 | if err != nil { 63 | panic(err) 64 | } 65 | ln, err := dtls.Listen("udp", addr, config) 66 | if err != nil { 67 | panic(err) 68 | } 69 | 70 | base.Info("listen DTLS server", addr) 71 | 72 | for { 73 | conn, err := ln.Accept() 74 | if err != nil { 75 | base.Error("DTLS Accept error", err) 76 | continue 77 | } 78 | 79 | go func() { 80 | // time.Sleep(1 * time.Second) 81 | cc := conn.(*dtls.Conn) 82 | did := hex.EncodeToString(cc.ConnectionState().SessionID) 83 | cSess := sessdata.Dtls2CSess(did) 84 | if cSess == nil { 85 | conn.Close() 86 | return 87 | } 88 | LinkDtls(conn, cSess) 89 | }() 90 | } 91 | } 92 | 93 | // https://github.com/pion/dtls/blob/master/session.go 94 | type sessionStore struct{} 95 | 96 | func (ms *sessionStore) Set(key []byte, s dtls.Session) error { 97 | return nil 98 | } 99 | 100 | func (ms *sessionStore) Get(key []byte) (dtls.Session, error) { 101 | k := hex.EncodeToString(key) 102 | secret := sessdata.Dtls2MasterSecret(k) 103 | if secret == "" { 104 | return dtls.Session{}, errors.New("Dtls2MasterSecret is nil") 105 | } 106 | 107 | masterSecret, _ := hex.DecodeString(secret) 108 | return dtls.Session{ID: key, Secret: masterSecret}, nil 109 | } 110 | 111 | func (ms *sessionStore) Del(key []byte) error { 112 | return nil 113 | } 114 | 115 | // 客户端和服务端映射 X-DTLS12-CipherSuite 116 | var dtlsCipherSuites = map[string]dtls.CipherSuiteID{ 117 | // "ECDHE-ECDSA-AES256-GCM-SHA384": dtls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 118 | // "ECDHE-ECDSA-AES128-GCM-SHA256": dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 119 | "ECDHE-RSA-AES256-GCM-SHA384": dtls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 120 | "ECDHE-RSA-AES128-GCM-SHA256": dtls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 121 | } 122 | 123 | func checkDtls12Ciphersuite(ciphersuite string) string { 124 | csArr := strings.Split(ciphersuite, ":") 125 | 126 | for _, v := range csArr { 127 | if _, ok := dtlsCipherSuites[v]; ok { 128 | return v 129 | } 130 | } 131 | // 返回默认值 132 | return "ECDHE-RSA-AES128-GCM-SHA256" 133 | } 134 | -------------------------------------------------------------------------------- /server/handler/link_base.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/xml" 5 | "os/exec" 6 | 7 | "github.com/bjdgyc/anylink/base" 8 | ) 9 | 10 | const BufferSize = 2048 11 | 12 | type ClientRequest struct { 13 | XMLName xml.Name `xml:"config-auth"` 14 | Client string `xml:"client,attr"` // 一般都是 vpn 15 | Type string `xml:"type,attr"` // 请求类型 init logout auth-reply 16 | AggregateAuthVersion string `xml:"aggregate-auth-version,attr"` // 一般都是 2 17 | Version string `xml:"version"` // 客户端版本号 18 | GroupAccess string `xml:"group-access"` // 请求的地址 19 | GroupSelect string `xml:"group-select"` // 选择的组名 20 | RemoteAddr string `xml:"remote_addr"` 21 | UserAgent string `xml:"user_agent"` 22 | SessionId string `xml:"session-id"` 23 | SessionToken string `xml:"session-token"` 24 | Auth auth `xml:"auth"` 25 | DeviceId deviceId `xml:"device-id"` 26 | MacAddressList macAddressList `xml:"mac-address-list"` 27 | } 28 | 29 | type auth struct { 30 | Username string `xml:"username"` 31 | Password string `xml:"password"` 32 | OtpSecret string `xml:"otp_secret"` 33 | SecondaryPassword string `xml:"secondary_password"` 34 | } 35 | 36 | type deviceId struct { 37 | ComputerName string `xml:"computer-name,attr"` 38 | DeviceType string `xml:"device-type,attr"` 39 | PlatformVersion string `xml:"platform-version,attr"` 40 | UniqueId string `xml:"unique-id,attr"` 41 | UniqueIdGlobal string `xml:"unique-id-global,attr"` 42 | } 43 | 44 | type macAddressList struct { 45 | MacAddress string `xml:"mac-address"` 46 | } 47 | 48 | func execCmd(cmdStrs []string) error { 49 | for _, cmdStr := range cmdStrs { 50 | cmd := exec.Command("sh", "-c", cmdStr) 51 | b, err := cmd.CombinedOutput() 52 | if err != nil { 53 | base.Error(cmdStr, string(b)) 54 | return err 55 | } 56 | } 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /server/handler/link_home.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/bjdgyc/anylink/admin" 9 | "github.com/bjdgyc/anylink/dbdata" 10 | ) 11 | 12 | func LinkHome(w http.ResponseWriter, r *http.Request) { 13 | // fmt.Println(r.RemoteAddr) 14 | // hu, _ := httputil.DumpRequest(r, true) 15 | // fmt.Println("DumpHome: ", string(hu)) 16 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 17 | w.Header().Del("X-Aggregate-Auth") 18 | 19 | connection := strings.ToLower(r.Header.Get("Connection")) 20 | userAgent := strings.ToLower(r.UserAgent()) 21 | if connection == "close" && (strings.Contains(userAgent, "anyconnect") || strings.Contains(userAgent, "openconnect")) { 22 | w.Header().Set("Connection", "close") 23 | w.WriteHeader(http.StatusBadRequest) 24 | return 25 | } 26 | index := &dbdata.SettingOther{} 27 | if err := dbdata.SettingGet(index); err != nil { 28 | return 29 | } 30 | 31 | if index.Homecode != http.StatusOK { 32 | w.WriteHeader(index.Homecode) 33 | return 34 | } 35 | w.WriteHeader(http.StatusOK) 36 | 37 | // if index.Homeindex == "" { 38 | // index.Homeindex = "AnyLink 是一个企业级远程办公 SSL VPN 软件,可以支持多人同时在线使用。" 39 | // } 40 | fmt.Fprintln(w, index.Homeindex) 41 | } 42 | 43 | func LinkOtpQr(w http.ResponseWriter, r *http.Request) { 44 | w.Header().Set("Cross-Origin-Resource-Policy", "cross-origin") 45 | 46 | _ = r.ParseForm() 47 | idS := r.FormValue("id") 48 | jwtToken := r.FormValue("jwt") 49 | data, err := admin.GetJwtData(jwtToken) 50 | if err != nil || idS != fmt.Sprint(data["id"]) { 51 | w.WriteHeader(http.StatusForbidden) 52 | return 53 | } 54 | 55 | admin.UserOtpQr(w, r) 56 | } 57 | -------------------------------------------------------------------------------- /server/handler/link_vtap.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | "strings" 8 | "syscall" 9 | "unsafe" 10 | 11 | "github.com/bjdgyc/anylink/base" 12 | "github.com/bjdgyc/anylink/pkg/utils" 13 | "github.com/bjdgyc/anylink/sessdata" 14 | gosysctl "github.com/lorenzosaino/go-sysctl" 15 | ) 16 | 17 | // link vtap 18 | const vTapPrefix = "lvtap" 19 | 20 | type Vtap struct { 21 | *os.File 22 | ifName string 23 | } 24 | 25 | func (v *Vtap) Close() error { 26 | v.File.Close() 27 | cmdstr := fmt.Sprintf("ip link del %s", v.ifName) 28 | return execCmd([]string{cmdstr}) 29 | } 30 | 31 | func checkMacvtap() { 32 | // 加载 macvtap 33 | base.CheckModOrLoad("macvtap") 34 | 35 | _setGateway() 36 | _checkTapIp(base.Cfg.Ipv4Master) 37 | 38 | ifName := "anylinkMacvtap" 39 | 40 | // 开启主网卡混杂模式 41 | cmdstr1 := fmt.Sprintf("ip link set dev %s promisc on", base.Cfg.Ipv4Master) 42 | // 测试 macvtap 功能 43 | cmdstr2 := fmt.Sprintf("ip link add link %s name %s type macvtap mode bridge", base.Cfg.Ipv4Master, ifName) 44 | cmdstr3 := fmt.Sprintf("ip link del %s", ifName) 45 | err := execCmd([]string{cmdstr1, cmdstr2, cmdstr3}) 46 | if err != nil { 47 | base.Fatal(err) 48 | } 49 | } 50 | 51 | // 创建 Macvtap 网卡 52 | func LinkMacvtap(cSess *sessdata.ConnSession) error { 53 | capL := sessdata.IpPool.IpLongMax - sessdata.IpPool.IpLongMin 54 | ipN := utils.Ip2long(cSess.IpAddr) % capL 55 | ifName := fmt.Sprintf("%s%d", vTapPrefix, ipN) 56 | 57 | cSess.SetIfName(ifName) 58 | 59 | cmdstr1 := fmt.Sprintf("ip link add link %s name %s type macvtap mode bridge", base.Cfg.Ipv4Master, ifName) 60 | alias := utils.ParseName(cSess.Group.Name + "." + cSess.Username) 61 | cmdstr2 := fmt.Sprintf("ip link set dev %s up mtu %d address %s alias %s", ifName, cSess.Mtu, cSess.MacHw, alias) 62 | 63 | err := execCmd([]string{cmdstr1, cmdstr2}) 64 | if err != nil { 65 | base.Error(err) 66 | return err 67 | } 68 | // cmdstr3 := fmt.Sprintf("sysctl -w net.ipv6.conf.%s.disable_ipv6=1", ifName) 69 | // execCmd([]string{cmdstr3}) 70 | err = gosysctl.Set(fmt.Sprintf("net.ipv6.conf.%s.disable_ipv6", ifName), "1") 71 | if err != nil { 72 | base.Warn(err) 73 | } 74 | 75 | return createVtap(cSess, ifName) 76 | } 77 | 78 | // func checkIpvtap() { 79 | 80 | // } 81 | 82 | // 创建 Ipvtap 网卡 83 | func LinkIpvtap(cSess *sessdata.ConnSession) error { 84 | return nil 85 | } 86 | 87 | type ifReq struct { 88 | Name [0x10]byte 89 | Flags uint16 90 | pad [0x28 - 0x10 - 2]byte 91 | } 92 | 93 | func createVtap(cSess *sessdata.ConnSession, ifName string) error { 94 | // 初始化 ifName 95 | inf, err := net.InterfaceByName(ifName) 96 | if err != nil { 97 | base.Error(err) 98 | return err 99 | } 100 | 101 | tName := fmt.Sprintf("/dev/tap%d", inf.Index) 102 | 103 | var fdInt int 104 | 105 | fdInt, err = syscall.Open(tName, syscall.O_RDWR|syscall.O_NONBLOCK, 0) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | var flags uint16 = syscall.IFF_TAP | syscall.IFF_NO_PI 111 | var req ifReq 112 | req.Flags = flags 113 | 114 | _, _, errno := syscall.Syscall( 115 | syscall.SYS_IOCTL, 116 | uintptr(fdInt), 117 | uintptr(syscall.TUNSETIFF), 118 | uintptr(unsafe.Pointer(&req)), 119 | ) 120 | if errno != 0 { 121 | return os.NewSyscallError("ioctl", errno) 122 | } 123 | 124 | file := os.NewFile(uintptr(fdInt), tName) 125 | ifce := &Vtap{file, ifName} 126 | 127 | go allTapRead(ifce, cSess) 128 | go allTapWrite(ifce, cSess) 129 | return nil 130 | } 131 | 132 | // 销毁未关闭的vtap 133 | func destroyVtap() { 134 | its, err := net.Interfaces() 135 | if err != nil { 136 | base.Error(err) 137 | return 138 | } 139 | for _, v := range its { 140 | if strings.HasPrefix(v.Name, vTapPrefix) { 141 | // 删除原来的网卡 142 | cmdstr := fmt.Sprintf("ip link del %s", v.Name) 143 | execCmd([]string{cmdstr}) 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /server/handler/payload.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/bjdgyc/anylink/base" 5 | "github.com/bjdgyc/anylink/dbdata" 6 | "github.com/bjdgyc/anylink/sessdata" 7 | "github.com/songgao/water/waterutil" 8 | ) 9 | 10 | func payloadIn(cSess *sessdata.ConnSession, pl *sessdata.Payload) bool { 11 | if pl.LType == sessdata.LTypeIPData && pl.PType == 0x00 { 12 | // 进行Acl规则判断 13 | check := checkLinkAcl(cSess.Group, pl) 14 | if !check { 15 | // 校验不通过直接丢弃 16 | return false 17 | } 18 | } 19 | 20 | closed := false 21 | select { 22 | case cSess.PayloadIn <- pl: 23 | case <-cSess.CloseChan: 24 | closed = true 25 | } 26 | 27 | return closed 28 | } 29 | 30 | func putPayloadInBefore(cSess *sessdata.ConnSession, pl *sessdata.Payload) { 31 | // 异步审计日志 32 | if base.Cfg.AuditInterval >= 0 { 33 | auditPayload.Add(cSess.Username, pl) 34 | return 35 | } 36 | putPayload(pl) 37 | } 38 | 39 | func payloadOut(cSess *sessdata.ConnSession, pl *sessdata.Payload) bool { 40 | dSess := cSess.GetDtlsSession() 41 | if dSess == nil { 42 | return payloadOutCstp(cSess, pl) 43 | } else { 44 | return payloadOutDtls(cSess, dSess, pl) 45 | } 46 | } 47 | 48 | func payloadOutCstp(cSess *sessdata.ConnSession, pl *sessdata.Payload) bool { 49 | closed := false 50 | 51 | select { 52 | case cSess.PayloadOutCstp <- pl: 53 | case <-cSess.CloseChan: 54 | closed = true 55 | } 56 | 57 | return closed 58 | } 59 | 60 | func payloadOutDtls(cSess *sessdata.ConnSession, dSess *sessdata.DtlsSession, pl *sessdata.Payload) bool { 61 | select { 62 | case cSess.PayloadOutDtls <- pl: 63 | case <-dSess.CloseChan: 64 | } 65 | 66 | return false 67 | } 68 | 69 | // Acl规则校验 70 | func checkLinkAcl(group *dbdata.Group, pl *sessdata.Payload) bool { 71 | if pl.LType == sessdata.LTypeIPData && pl.PType == 0x00 && len(group.LinkAcl) > 0 { 72 | } else { 73 | return true 74 | } 75 | 76 | ipDst := waterutil.IPv4Destination(pl.Data) 77 | ipPort := waterutil.IPv4DestinationPort(pl.Data) 78 | ipProto := waterutil.IPv4Protocol(pl.Data) 79 | // fmt.Println("sent:", ip_dst, ip_port) 80 | 81 | // 优先放行dns端口 82 | for _, v := range group.ClientDns { 83 | if v.Val == ipDst.String() && ipPort == 53 { 84 | return true 85 | } 86 | } 87 | 88 | for _, v := range group.LinkAcl { 89 | // 放行允许ip的ping 90 | // if v.Ports == nil || len(v.Ports) == 0 { 91 | // //单端口历史数据兼容 92 | // port := uint16(v.Port.(float64)) 93 | // if port == ipPort || port == 0 || ipProto == waterutil.ICMP { 94 | // if v.Action == dbdata.Allow { 95 | // return true 96 | // } else { 97 | // return false 98 | // } 99 | // } 100 | // } else { 101 | 102 | // 先判断协议 103 | // 兼容旧数据 v.Protocol == "" 104 | if v.Protocol == "" || v.Protocol == dbdata.ALL || v.IpProto == ipProto { 105 | // 循环判断ip和端口 106 | if v.IpNet.Contains(ipDst) { 107 | // icmp 不判断端口 108 | if ipProto == waterutil.ICMP { 109 | if v.Action == dbdata.Allow { 110 | return true 111 | } else { 112 | return false 113 | } 114 | } 115 | 116 | if dbdata.ContainsInPorts(v.Ports, ipPort) || dbdata.ContainsInPorts(v.Ports, 0) { 117 | if v.Action == dbdata.Allow { 118 | // log.Println(dbdata.Allow, v.Ports) 119 | return true 120 | } else { 121 | // log.Println(dbdata.Deny, v.Ports) 122 | return false 123 | } 124 | } 125 | } 126 | } 127 | } 128 | 129 | return false 130 | } 131 | -------------------------------------------------------------------------------- /server/handler/pool.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/bjdgyc/anylink/base" 7 | "github.com/bjdgyc/anylink/sessdata" 8 | ) 9 | 10 | // 不允许直接修改 11 | // [6] => PType 12 | var plHeader = []byte{ 13 | 'S', 'T', 'F', 1, 14 | 0x00, 0x00, /* Length */ 15 | 0x00, /* Type */ 16 | 0x00, /* Unknown */ 17 | } 18 | 19 | var plPool = sync.Pool{ 20 | New: func() interface{} { 21 | b := make([]byte, BufferSize) 22 | pl := sessdata.Payload{ 23 | LType: sessdata.LTypeIPData, 24 | PType: 0x00, 25 | Data: b, 26 | } 27 | // fmt.Println("plPool-init", len(pl.Data), cap(pl.Data)) 28 | return &pl 29 | }, 30 | } 31 | 32 | func getPayload() *sessdata.Payload { 33 | pl := plPool.Get().(*sessdata.Payload) 34 | return pl 35 | } 36 | 37 | func putPayload(pl *sessdata.Payload) { 38 | // 错误数据丢弃 39 | if cap(pl.Data) != BufferSize { 40 | base.Warn("payload cap is err", cap(pl.Data)) 41 | return 42 | } 43 | 44 | pl.LType = sessdata.LTypeIPData 45 | pl.PType = 0x00 46 | pl.Data = pl.Data[:BufferSize] 47 | plPool.Put(pl) 48 | } 49 | 50 | var bytePool = sync.Pool{ 51 | New: func() interface{} { 52 | b := make([]byte, BufferSize) 53 | // fmt.Println("bytePool-init") 54 | return &b 55 | }, 56 | } 57 | 58 | func getByteZero() *[]byte { 59 | b := bytePool.Get().(*[]byte) 60 | *b = (*b)[:0] 61 | return b 62 | } 63 | 64 | func getByteFull() *[]byte { 65 | b := bytePool.Get().(*[]byte) 66 | return b 67 | } 68 | func putByte(b *[]byte) { 69 | *b = (*b)[:BufferSize] 70 | bytePool.Put(b) 71 | } 72 | 73 | // 长度 34 小对象 74 | var byte34Pool = sync.Pool{ 75 | New: func() interface{} { 76 | b := make([]byte, 34) 77 | return &b 78 | }, 79 | } 80 | 81 | func getByte34() *[]byte { 82 | b := byte34Pool.Get().(*[]byte) 83 | return b 84 | } 85 | 86 | func putByte34(b *[]byte) { 87 | *b = (*b)[:34] 88 | byte34Pool.Put(b) 89 | } 90 | 91 | type BufferPool struct { 92 | sync.Pool 93 | } 94 | 95 | // 长度 51 小对象 96 | var byte51Pool = sync.Pool{ 97 | New: func() interface{} { 98 | b := make([]byte, 51) 99 | return &b 100 | }, 101 | } 102 | 103 | func getByte51() *[]byte { 104 | b := byte51Pool.Get().(*[]byte) 105 | return b 106 | } 107 | 108 | func putByte51(b *[]byte) { 109 | *b = (*b)[:51] 110 | byte51Pool.Put(b) 111 | } 112 | -------------------------------------------------------------------------------- /server/handler/pool_test.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // go test -bench=. -benchmem 8 | 9 | // 去除数据头 10 | func BenchmarkHeaderCopy(b *testing.B) { 11 | l := 1500 12 | for i := 0; i < b.N; i++ { 13 | b.StopTimer() 14 | pl := getPayload() 15 | // 初始化数据 16 | pl.Data = pl.Data[:l] 17 | 18 | b.StartTimer() 19 | dataLen := l - 8 20 | copy(pl.Data, pl.Data[8:8+dataLen]) 21 | // 更新切片长度 22 | pl.Data = pl.Data[:dataLen] 23 | b.StopTimer() 24 | 25 | putPayload(pl) 26 | } 27 | } 28 | 29 | func BenchmarkHeaderAppend(b *testing.B) { 30 | l := 1500 31 | for i := 0; i < b.N; i++ { 32 | b.StopTimer() 33 | pl := getPayload() 34 | // 初始化数据 35 | pl.Data = pl.Data[:l] 36 | 37 | b.StartTimer() 38 | dataLen := l - 8 39 | pl.Data = append(pl.Data[:0], pl.Data[:8+dataLen]...) 40 | b.StopTimer() 41 | 42 | putPayload(pl) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /server/handler/start.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "log" 7 | "os" 8 | 9 | "github.com/bjdgyc/anylink/admin" 10 | "github.com/bjdgyc/anylink/base" 11 | "github.com/bjdgyc/anylink/cron" 12 | "github.com/bjdgyc/anylink/dbdata" 13 | "github.com/bjdgyc/anylink/sessdata" 14 | gosysctl "github.com/lorenzosaino/go-sysctl" 15 | ) 16 | 17 | func Start() { 18 | dbdata.Start() 19 | sessdata.Start() 20 | cron.Start() 21 | 22 | admin.InitLockManager() // 初始化防爆破定时器和IP白名单 23 | 24 | // 开启服务器转发 25 | err := gosysctl.Set("net.ipv4.ip_forward", "1") 26 | if err != nil { 27 | base.Warn(err) 28 | } 29 | 30 | val, err := gosysctl.Get("net.ipv4.ip_forward") 31 | if val != "1" { 32 | log.Fatal("Please exec 'sysctl -w net.ipv4.ip_forward=1' ") 33 | } 34 | // os.Exit(0) 35 | // execCmd([]string{"sysctl -w net.ipv4.ip_forward=1"}) 36 | 37 | switch base.Cfg.LinkMode { 38 | case base.LinkModeTUN: 39 | checkTun() 40 | case base.LinkModeTAP: 41 | checkTap() 42 | case base.LinkModeMacvtap: 43 | checkMacvtap() 44 | default: 45 | base.Fatal("LinkMode is err") 46 | } 47 | 48 | // 计算profile.xml的hash 49 | b, err := os.ReadFile(base.Cfg.Profile) 50 | if err != nil { 51 | panic(err) 52 | } 53 | ha := sha1.Sum(b) 54 | profileHash = hex.EncodeToString(ha[:]) 55 | 56 | go admin.StartAdmin() 57 | go startTls() 58 | go startDtls() 59 | 60 | go logAuditBatch() 61 | } 62 | 63 | func Stop() { 64 | _ = dbdata.Stop() 65 | destroyVtap() 66 | } 67 | -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | // AnyLink 是一个企业级远程办公vpn软件,可以支持多人同时在线使用。 2 | 3 | //go:build !windows 4 | // +build !windows 5 | 6 | package main 7 | 8 | import ( 9 | "embed" 10 | "os" 11 | "os/signal" 12 | "syscall" 13 | 14 | "github.com/bjdgyc/anylink/admin" 15 | "github.com/bjdgyc/anylink/base" 16 | "github.com/bjdgyc/anylink/handler" 17 | ) 18 | 19 | //go:embed ui 20 | var uiData embed.FS 21 | 22 | // 程序版本 23 | var ( 24 | appVer string 25 | commitId string 26 | buildDate string 27 | ) 28 | 29 | func main() { 30 | admin.UiData = uiData 31 | base.APP_VER = appVer 32 | base.CommitId = commitId 33 | base.BuildDate = buildDate 34 | 35 | base.Start() 36 | handler.Start() 37 | 38 | signalWatch() 39 | } 40 | 41 | func signalWatch() { 42 | base.Info("Server pid: ", os.Getpid()) 43 | 44 | sigs := make(chan os.Signal, 1) 45 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGALRM, syscall.SIGUSR2) 46 | for { 47 | sig := <-sigs 48 | base.Info("Get signal:", sig) 49 | switch sig { 50 | case syscall.SIGUSR2: 51 | // reload 52 | base.Info("Reload") 53 | default: 54 | // stop 55 | base.Info("Stop") 56 | handler.Stop() 57 | return 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /server/pkg/arpdis/addr.go: -------------------------------------------------------------------------------- 1 | package arpdis 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | "time" 7 | 8 | "github.com/bjdgyc/anylink/pkg/utils" 9 | ) 10 | 11 | const ( 12 | StaleTimeNormal = time.Minute * 5 13 | StaleTimeUnreachable = time.Minute * 10 14 | 15 | TypeNormal = 0 16 | TypeUnreachable = 1 17 | TypeStatic = 2 18 | ) 19 | 20 | var ( 21 | table = make(map[string]*Addr, 128) 22 | tableMu sync.RWMutex 23 | ) 24 | 25 | type Addr struct { 26 | IP net.IP 27 | HardwareAddr net.HardwareAddr 28 | disTime time.Time 29 | Type int8 30 | } 31 | 32 | func Lookup(ip net.IP, onlyTable bool) *Addr { 33 | addr := tableLookup(ip.To4()) 34 | if addr != nil || onlyTable { 35 | return addr 36 | } 37 | 38 | addr = doLookup(ip.To4()) 39 | Add(addr) 40 | return addr 41 | } 42 | 43 | // Add adds a IP-MAC map to a runtime ARP table. 44 | func tableLookup(ip net.IP) *Addr { 45 | tableMu.RLock() 46 | addr := table[ip.To4().String()] 47 | tableMu.RUnlock() 48 | if addr == nil { 49 | return nil 50 | } 51 | 52 | // 判断老化过期时间 53 | tSub := utils.NowSec().Sub(addr.disTime) 54 | switch addr.Type { 55 | case TypeStatic: 56 | case TypeNormal: 57 | if tSub > StaleTimeNormal { 58 | return nil 59 | } 60 | case TypeUnreachable: 61 | if tSub > StaleTimeUnreachable { 62 | return nil 63 | } 64 | } 65 | 66 | return addr 67 | } 68 | 69 | // Add adds a IP-MAC map to a runtime ARP table. 70 | func Add(addr *Addr) { 71 | if addr == nil { 72 | return 73 | } 74 | if addr.disTime.IsZero() { 75 | addr.disTime = utils.NowSec() 76 | } 77 | ip := addr.IP.To4().String() 78 | tableMu.Lock() 79 | defer tableMu.Unlock() 80 | if a, ok := table[ip]; ok { 81 | // 静态地址只能设置一次 82 | if a.Type == TypeStatic { 83 | return 84 | } 85 | } 86 | table[ip] = addr 87 | } 88 | 89 | // Delete removes an IP from the runtime ARP table. 90 | func Delete(ip net.IP) { 91 | tableMu.Lock() 92 | defer tableMu.Unlock() 93 | delete(table, ip.To4().String()) 94 | } 95 | 96 | // List returns the current runtime ARP table. 97 | func List() map[string]*Addr { 98 | tableMu.RLock() 99 | defer tableMu.RUnlock() 100 | return table 101 | } 102 | -------------------------------------------------------------------------------- /server/pkg/arpdis/addr_test.go: -------------------------------------------------------------------------------- 1 | package arpdis 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestLookup(t *testing.T) { 12 | assert := assert.New(t) 13 | ip := net.IPv4(192, 168, 10, 2) 14 | hw, _ := net.ParseMAC("08:00:27:a0:17:42") 15 | now := time.Now() 16 | addr1 := &Addr{ 17 | IP: ip, 18 | HardwareAddr: hw, 19 | Type: TypeStatic, 20 | disTime: now, 21 | } 22 | Add(addr1) 23 | addr2 := Lookup(ip, true) 24 | assert.Equal(addr1, addr2) 25 | addr3 := &Addr{ 26 | IP: ip, 27 | HardwareAddr: hw, 28 | Type: TypeNormal, 29 | disTime: now, 30 | } 31 | Add(addr3) 32 | addr4 := Lookup(ip, true) 33 | // 静态地址只能设置一次 34 | assert.NotEqual(addr3, addr4) 35 | } 36 | -------------------------------------------------------------------------------- /server/pkg/arpdis/arp.go: -------------------------------------------------------------------------------- 1 | package arpdis 2 | 3 | // Reference: github.com/malfunkt/arpfox 4 | // TODO now only support IPv4 5 | 6 | import ( 7 | "github.com/google/gopacket" 8 | "github.com/google/gopacket/layers" 9 | ) 10 | 11 | var defaultSerializeOpts = gopacket.SerializeOptions{ 12 | FixLengths: true, 13 | ComputeChecksums: true, 14 | } 15 | 16 | // NewARPRequest creates a bew ARP packet of type "request. 17 | func NewARPRequest(src *Addr, dst *Addr) ([]byte, error) { 18 | return buildPacket(src, dst, layers.ARPRequest) 19 | } 20 | 21 | // NewARPReply creates a new ARP packet of type "reply". 22 | func NewARPReply(src *Addr, dst *Addr) ([]byte, error) { 23 | return buildPacket(src, dst, layers.ARPReply) 24 | } 25 | 26 | // buildPacket creates an template ARP packet with the given source and 27 | // destination. 28 | func buildPacket(src *Addr, dst *Addr, opt uint16) ([]byte, error) { 29 | ether := layers.Ethernet{ 30 | EthernetType: layers.EthernetTypeARP, 31 | SrcMAC: src.HardwareAddr, 32 | DstMAC: dst.HardwareAddr, 33 | } 34 | arp := layers.ARP{ 35 | AddrType: layers.LinkTypeEthernet, 36 | Protocol: layers.EthernetTypeIPv4, 37 | 38 | HwAddressSize: 6, 39 | ProtAddressSize: 4, 40 | Operation: opt, 41 | 42 | SourceHwAddress: src.HardwareAddr, 43 | SourceProtAddress: src.IP.To4(), 44 | 45 | DstHwAddress: dst.HardwareAddr, 46 | DstProtAddress: dst.IP.To4(), 47 | } 48 | 49 | buf := gopacket.NewSerializeBuffer() 50 | err := gopacket.SerializeLayers(buf, defaultSerializeOpts, ðer, &arp) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | return buf.Bytes(), nil 56 | } 57 | -------------------------------------------------------------------------------- /server/pkg/arpdis/icmp.go: -------------------------------------------------------------------------------- 1 | package arpdis 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "os" 7 | "time" 8 | 9 | "golang.org/x/net/icmp" 10 | "golang.org/x/net/ipv4" 11 | ) 12 | 13 | const ( 14 | ProtocolICMP = 1 15 | ProtocolIPv6ICMP = 58 16 | ) 17 | 18 | func doPing(ip string) error { 19 | raddr, _ := net.ResolveIPAddr("ip4:icmp", ip) 20 | conn, err := icmp.ListenPacket("ip4:icmp", "") 21 | if err != nil { 22 | return err 23 | } 24 | 25 | ipv4Conn := conn.IPv4PacketConn() 26 | // 限制跳跃数 27 | err = ipv4Conn.SetTTL(10) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | msg := &icmp.Message{ 33 | Type: ipv4.ICMPTypeEcho, 34 | Code: 0, 35 | Body: &icmp.Echo{ 36 | ID: os.Getpid() & 0xffff, 37 | Seq: 1, 38 | Data: timeToBytes(time.Now()), 39 | }, 40 | } 41 | 42 | b, err := msg.Marshal(nil) 43 | if err != nil { 44 | return err 45 | } 46 | _, err = conn.WriteTo(b, raddr) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | _ = conn.SetReadDeadline(time.Now().Add(time.Second * 2)) 52 | 53 | for { 54 | buf := make([]byte, 512) 55 | n, dst, err := conn.ReadFrom(buf) 56 | if err != nil { 57 | return err 58 | } 59 | if dst.String() != ip { 60 | continue 61 | } 62 | 63 | var result *icmp.Message 64 | result, err = icmp.ParseMessage(ProtocolICMP, buf[:n]) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | switch result.Type { 70 | case ipv4.ICMPTypeEchoReply: 71 | // success 72 | if rply, ok := result.Body.(*icmp.Echo); ok { 73 | _ = rply 74 | // log.Printf("%+v \n", rply) 75 | } 76 | return nil 77 | 78 | // case ipv4.ICMPTypeTimeExceeded: 79 | // case ipv4.ICMPTypeDestinationUnreachable: 80 | default: 81 | return errors.New("DestinationUnreachable") 82 | } 83 | } 84 | } 85 | 86 | func timeToBytes(t time.Time) []byte { 87 | nsec := t.UnixNano() 88 | b := make([]byte, 8) 89 | for i := uint8(0); i < 8; i++ { 90 | b[i] = byte((nsec >> ((7 - i) * 8)) & 0xff) 91 | } 92 | return b 93 | } 94 | 95 | func bytesToTime(b []byte) time.Time { 96 | var nsec int64 97 | for i := uint8(0); i < 8; i++ { 98 | nsec += int64(b[i]) << ((7 - i) * 8) 99 | } 100 | return time.Unix(nsec/1000000000, nsec%1000000000) 101 | } 102 | -------------------------------------------------------------------------------- /server/pkg/arpdis/lookup.go: -------------------------------------------------------------------------------- 1 | // Currently only Darwin and Linux support this. 2 | 3 | package arpdis 4 | 5 | import ( 6 | "log" 7 | "net" 8 | "os/exec" 9 | "strings" 10 | ) 11 | 12 | func doLookup(ip net.IP) *Addr { 13 | // ping := exec.Command("ping", "-c1", "-t1", ip.String()) 14 | // if err := ping.Run(); err != nil { 15 | // addr := &Addr{IP: ip, Type: TypeUnreachable} 16 | // return addr 17 | // } 18 | 19 | err := doPing(ip.String()) 20 | if err != nil { 21 | // log.Println(err) 22 | addr := &Addr{IP: net.IPv4(1, 2, 3, 4), Type: TypeUnreachable} 23 | copy(addr.IP, ip) 24 | return addr 25 | } 26 | 27 | return doArpShow(ip) 28 | } 29 | 30 | func doArpShow(ip net.IP) *Addr { 31 | cmd := exec.Command("ip", "n", "show", ip.String()) 32 | out, err := cmd.Output() 33 | if err != nil { 34 | log.Println("lookup show", err) 35 | return nil 36 | } 37 | 38 | // os.Open("/proc/net/arp") 39 | // 192.168.1.2 0x1 0x2 e0:94:67:e2:42:5d * eth0 40 | // 192.168.1.2 dev eth0 lladdr 08:00:27:94:a5:a4 STALE 41 | outS := strings.ReplaceAll(string(out), " ", " ") 42 | outS = strings.TrimSpace(outS) 43 | arpArr := strings.Split(outS, " ") 44 | if len(arpArr) != 6 { 45 | log.Println("lookup arpArr", outS, ip) 46 | return nil 47 | } 48 | mac, err := net.ParseMAC(arpArr[4]) 49 | if err != nil { 50 | log.Println("lookup mac", outS, err) 51 | return nil 52 | } 53 | 54 | addr := &Addr{IP: net.IPv4(1, 2, 3, 4), HardwareAddr: mac} 55 | copy(addr.IP, ip) 56 | return addr 57 | } 58 | 59 | // IP address HW type Flags HW address Mask Device 60 | // 172.23.24.12 0x1 0x2 00:e0:4c:73:5c:48 * anylink0 61 | // 172.23.24.1 0x1 0x2 3c:8c:40:a0:7a:2d * anylink0 62 | // 172.23.24.13 0x1 0x2 00:1c:42:4d:33:46 * anylink0 63 | // 172.23.24.2 0x1 0x0 00:00:00:00:00:00 * anylink0 64 | // 172.23.24.14 0x1 0x0 00:00:00:00:00:00 * anylink0 65 | -------------------------------------------------------------------------------- /server/pkg/utils/ip.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/binary" 5 | "net" 6 | ) 7 | 8 | func Long2ip(i uint32) net.IP { 9 | ip := make([]byte, 4) 10 | binary.BigEndian.PutUint32(ip, i) 11 | return ip 12 | } 13 | 14 | func Ip2long(ip net.IP) uint32 { 15 | ip = ip.To4() 16 | return binary.BigEndian.Uint32(ip) 17 | } 18 | -------------------------------------------------------------------------------- /server/pkg/utils/maps.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "sync" 5 | 6 | cmap "github.com/orcaman/concurrent-map" 7 | ) 8 | 9 | type IMaps interface { 10 | Set(key string, val interface{}) 11 | Get(key string) (interface{}, bool) 12 | Del(key string) 13 | } 14 | 15 | /** 16 | * 基础的Map结构 17 | * 18 | */ 19 | type BaseMap struct { 20 | m map[string]interface{} 21 | } 22 | 23 | func (m *BaseMap) Set(key string, value interface{}) { 24 | m.m[key] = value 25 | } 26 | func (m *BaseMap) Get(key string) (interface{}, bool) { 27 | v, ok := m.m[key] 28 | return v, ok 29 | } 30 | func (m *BaseMap) Del(key string) { 31 | delete(m.m, key) 32 | } 33 | 34 | /** 35 | * CMap 并发结构 36 | * 37 | */ 38 | type ConcurrentMap struct { 39 | m cmap.ConcurrentMap 40 | } 41 | 42 | func (m *ConcurrentMap) Set(key string, value interface{}) { 43 | m.m.Set(key, value) 44 | } 45 | 46 | func (m *ConcurrentMap) Get(key string) (interface{}, bool) { 47 | return m.m.Get(key) 48 | } 49 | 50 | func (m *ConcurrentMap) Del(key string) { 51 | m.m.Remove(key) 52 | } 53 | 54 | /** 55 | * Map 读写结构 56 | * 57 | */ 58 | type RWLockMap struct { 59 | m map[string]interface{} 60 | lock sync.RWMutex 61 | } 62 | 63 | func (m *RWLockMap) Set(key string, value interface{}) { 64 | m.lock.Lock() 65 | defer m.lock.Unlock() 66 | m.m[key] = value 67 | } 68 | 69 | func (m *RWLockMap) Get(key string) (interface{}, bool) { 70 | m.lock.RLock() 71 | defer m.lock.RUnlock() 72 | v, ok := m.m[key] 73 | return v, ok 74 | } 75 | 76 | func (m *RWLockMap) Del(key string) { 77 | m.lock.Lock() 78 | defer m.lock.Unlock() 79 | delete(m.m, key) 80 | } 81 | 82 | /** 83 | * sync.Map 结构 84 | * 85 | */ 86 | type SyncMap struct { 87 | m sync.Map 88 | } 89 | 90 | func (m *SyncMap) Set(key string, val interface{}) { 91 | m.m.Store(key, val) 92 | } 93 | 94 | func (m *SyncMap) Get(key string) (interface{}, bool) { 95 | return m.m.Load(key) 96 | } 97 | 98 | func (m *SyncMap) Del(key string) { 99 | m.m.Delete(key) 100 | } 101 | 102 | func NewMap(name string, len int) IMaps { 103 | switch name { 104 | case "cmap": 105 | return &ConcurrentMap{m: cmap.New()} 106 | case "rwmap": 107 | m := make(map[string]interface{}, len) 108 | return &RWLockMap{m: m} 109 | case "syncmap": 110 | return &SyncMap{} 111 | default: 112 | m := make(map[string]interface{}, len) 113 | return &BaseMap{m: m} 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /server/pkg/utils/maps_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strconv" 5 | "sync" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | const ( 12 | NumOfReader = 200 13 | NumOfWriter = 100 14 | ) 15 | 16 | func TestMaps(t *testing.T) { 17 | assert := assert.New(t) 18 | var ipAuditMap IMaps 19 | key := "one" 20 | value := 100 21 | 22 | testMapData := map[string]int{"basemap": 512, "cmap": 0, "rwmap": 512, "syncmap": 0} 23 | for name, len := range testMapData { 24 | ipAuditMap = NewMap(name, len) 25 | ipAuditMap.Set(key, value) 26 | v, ok := ipAuditMap.Get(key) 27 | assert.Equal(v.(int), value) 28 | assert.True(ok) 29 | ipAuditMap.Del(key) 30 | v, ok = ipAuditMap.Get(key) 31 | assert.Nil(v) 32 | assert.False(ok) 33 | } 34 | } 35 | 36 | func benchmarkMap(b *testing.B, hm IMaps) { 37 | for i := 0; i < b.N; i++ { 38 | var wg sync.WaitGroup 39 | for i := 0; i < NumOfWriter; i++ { 40 | wg.Add(1) 41 | go func() { 42 | for i := 0; i < 100; i++ { 43 | hm.Set(strconv.Itoa(i), i*i) 44 | hm.Set(strconv.Itoa(i), i*i) 45 | hm.Del(strconv.Itoa(i)) 46 | } 47 | wg.Done() 48 | }() 49 | } 50 | for i := 0; i < NumOfReader; i++ { 51 | wg.Add(1) 52 | go func() { 53 | for i := 0; i < 100; i++ { 54 | hm.Get(strconv.Itoa(i)) 55 | } 56 | wg.Done() 57 | }() 58 | } 59 | wg.Wait() 60 | } 61 | } 62 | 63 | func BenchmarkMaps(b *testing.B) { 64 | b.Run("RW map", func(b *testing.B) { 65 | myMap := NewMap("rwmap", 512) 66 | benchmarkMap(b, myMap) 67 | }) 68 | b.Run("Concurrent map", func(b *testing.B) { 69 | myMap := NewMap("cmap", 0) 70 | benchmarkMap(b, myMap) 71 | }) 72 | b.Run("Sync map", func(b *testing.B) { 73 | myMap := NewMap("syncmap", 0) 74 | benchmarkMap(b, myMap) 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /server/pkg/utils/password_hash.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | mt "math/rand" 7 | 8 | "golang.org/x/crypto/bcrypt" 9 | ) 10 | 11 | func PasswordHash(password string) (string, error) { 12 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 13 | return string(bytes), err 14 | } 15 | 16 | func PasswordVerify(password, hash string) bool { 17 | // 保留老用户明文验证 18 | if len(hash) != 60 { 19 | return password == hash 20 | } 21 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) 22 | return err == nil 23 | } 24 | 25 | // $sha-256$salt-key$hash-abcd 26 | // $sha-512$salt-key$hash-abcd 27 | const ( 28 | saltSize = 16 29 | delmiter = "$" 30 | ) 31 | 32 | func RandSecret(min int, max int) (string, error) { 33 | rb := make([]byte, randInt(min, max)) 34 | _, err := rand.Read(rb) 35 | if err != nil { 36 | return "", err 37 | } 38 | 39 | return base64.URLEncoding.EncodeToString(rb), nil 40 | } 41 | 42 | func randInt(min int, max int) int { 43 | return min + mt.Intn(max-min) 44 | } 45 | -------------------------------------------------------------------------------- /server/pkg/utils/secure_header.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "net/http" 4 | 5 | // SetSecureHeader 设置安全的header头 6 | // https://blog.csdn.net/liwan09/article/details/130248003 7 | // https://zhuanlan.zhihu.com/p/335165168 8 | func SetSecureHeader(w http.ResponseWriter) { 9 | // Content-Length Date 默认已经存在 10 | w.Header().Set("Server", "AnyLinkOpenSource") 11 | // w.Header().Set("Content-Type", "text/html; charset=utf-8") 12 | // w.Header().Set("Transfer-Encoding", "chunked") 13 | w.Header().Set("X-Aggregate-Auth", "1") 14 | 15 | w.Header().Set("Cache-Control", "no-store,no-cache") 16 | w.Header().Set("Pragma", "no-cache") 17 | w.Header().Set("Connection", "keep-alive") 18 | w.Header().Set("X-Frame-Options", "SAMEORIGIN") 19 | w.Header().Set("X-Content-Type-Options", "nosniff") 20 | w.Header().Set("X-Download-Options", "noopen") 21 | w.Header().Set("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; frame-ancestors 'self'; base-uri 'self'; block-all-mixed-content") 22 | w.Header().Set("X-Permitted-Cross-Domain-Policies", "none") 23 | w.Header().Set("Referrer-Policy", "same-origin") 24 | w.Header().Set("Cross-Origin-Embedder-Policy", "require-corp") 25 | w.Header().Set("Cross-Origin-Opener-Policy", "same-origin") 26 | w.Header().Set("Cross-Origin-Resource-Policy", "same-origin") 27 | w.Header().Set("X-XSS-Protection", "1;mode=block") 28 | w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") 29 | 30 | // w.Header().Set("Clear-Site-Data", "cache,cookies,storage") 31 | 32 | } 33 | -------------------------------------------------------------------------------- /server/pkg/utils/unsafe.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "unsafe" 5 | ) 6 | 7 | // BytesToString converts byte slice to string. 8 | func BytesToString(b []byte) string { 9 | return *(*string)(unsafe.Pointer(&b)) 10 | } 11 | 12 | // StringToBytes converts string to byte slice. 13 | func StringToBytes(s string) []byte { 14 | return *(*[]byte)(unsafe.Pointer( 15 | &struct { 16 | string 17 | Cap int 18 | }{s, len(s)}, 19 | )) 20 | } 21 | -------------------------------------------------------------------------------- /server/pkg/utils/util.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | crand "crypto/rand" 5 | "encoding/hex" 6 | "fmt" 7 | "log" 8 | "math/rand" 9 | "strings" 10 | "sync/atomic" 11 | "time" 12 | ) 13 | 14 | var ( 15 | // 每秒时间缓存 16 | timeNowSec = &atomic.Value{} 17 | ) 18 | 19 | func init() { 20 | rand.Seed(time.Now().UnixNano()) 21 | 22 | timeNowSec.Store(time.Now()) 23 | go func() { 24 | tick := time.NewTicker(time.Second * 1) 25 | for c := range tick.C { 26 | timeNowSec.Store(c) 27 | } 28 | }() 29 | } 30 | 31 | func NowSec() time.Time { 32 | t := timeNowSec.Load() 33 | return t.(time.Time) 34 | } 35 | 36 | func InArrStr(arr []string, str string) bool { 37 | for _, d := range arr { 38 | if d == str { 39 | return true 40 | } 41 | } 42 | return false 43 | } 44 | 45 | const ( 46 | KB = 1024 47 | MB = 1024 * KB 48 | GB = 1024 * MB 49 | TB = 1024 * GB 50 | PB = 1024 * TB 51 | ) 52 | 53 | func HumanByte(bf interface{}) string { 54 | var hb string 55 | var bAll float64 56 | switch bi := bf.(type) { 57 | case int: 58 | bAll = float64(bi) 59 | case int32: 60 | bAll = float64(bi) 61 | case uint32: 62 | bAll = float64(bi) 63 | case int64: 64 | bAll = float64(bi) 65 | case uint64: 66 | bAll = float64(bi) 67 | case float64: 68 | bAll = float64(bi) 69 | } 70 | 71 | switch { 72 | case bAll >= TB: 73 | hb = fmt.Sprintf("%0.2f TB", bAll/TB) 74 | case bAll >= GB: 75 | hb = fmt.Sprintf("%0.2f GB", bAll/GB) 76 | case bAll >= MB: 77 | hb = fmt.Sprintf("%0.2f MB", bAll/MB) 78 | case bAll >= KB: 79 | hb = fmt.Sprintf("%0.2f KB", bAll/KB) 80 | default: 81 | hb = fmt.Sprintf("%0.2f B", bAll) 82 | } 83 | 84 | return hb 85 | } 86 | 87 | func RandomRunes(length int) string { 88 | letterRunes := []rune("abcdefghijklmnpqrstuvwxy1234567890") 89 | bytes := make([]rune, length) 90 | for i := range bytes { 91 | bytes[i] = letterRunes[rand.Intn(len(letterRunes))] 92 | } 93 | 94 | return string(bytes) 95 | } 96 | 97 | func RandomHex(length int) string { 98 | b := make([]byte, length) 99 | _, err := crand.Read(b) 100 | if err != nil { 101 | log.Println(err) 102 | return "" 103 | } 104 | 105 | return hex.EncodeToString(b) 106 | } 107 | 108 | func ParseName(name string) string { 109 | name = strings.ReplaceAll(name, " ", "-") 110 | name = strings.ReplaceAll(name, "'", "-") 111 | name = strings.ReplaceAll(name, "\"", "-") 112 | name = strings.ReplaceAll(name, ";", "-") 113 | return name 114 | } 115 | -------------------------------------------------------------------------------- /server/pkg/utils/util_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestInArrStr(t *testing.T) { 10 | assert := assert.New(t) 11 | arr := []string{"a", "b", "c"} 12 | assert.True(InArrStr(arr, "b")) 13 | assert.False(InArrStr(arr, "d")) 14 | } 15 | 16 | func TestHumanByte(t *testing.T) { 17 | assert := assert.New(t) 18 | var s string 19 | s = HumanByte(999) 20 | assert.Equal(s, "999.00 B") 21 | s = HumanByte(10256) 22 | assert.Equal(s, "10.02 KB") 23 | s = HumanByte(99 * 1024 * 1024) 24 | assert.Equal(s, "99.00 MB") 25 | s = HumanByte(1023 * 1024 * 1024) 26 | assert.Equal(s, "1023.00 MB") 27 | s = HumanByte(1024 * 1024 * 1024) 28 | assert.Equal(s, "1.00 GB") 29 | } 30 | -------------------------------------------------------------------------------- /server/sessdata/compress.go: -------------------------------------------------------------------------------- 1 | package sessdata 2 | 3 | import ( 4 | "github.com/lanrenwo/lzsgo" 5 | ) 6 | 7 | type CmpEncoding interface { 8 | Compress(src []byte, dst []byte) (int, error) 9 | Uncompress(src []byte, dst []byte) (int, error) 10 | } 11 | 12 | type LzsgoCmp struct { 13 | } 14 | 15 | func (l LzsgoCmp) Compress(src []byte, dst []byte) (int, error) { 16 | n, err := lzsgo.Compress(src, dst) 17 | return n, err 18 | } 19 | 20 | func (l LzsgoCmp) Uncompress(src []byte, dst []byte) (int, error) { 21 | n, err := lzsgo.Uncompress(src, dst) 22 | return n, err 23 | } 24 | 25 | // type Lz4Cmp struct { 26 | // c lz4.Compressor 27 | // } 28 | 29 | // func (l Lz4Cmp) Compress(src []byte, dst []byte) (int, error) { 30 | // return l.c.CompressBlock(src, dst) 31 | // } 32 | 33 | // func (l Lz4Cmp) Uncompress(src []byte, dst []byte) (int, error) { 34 | // return lz4.UncompressBlock(src, dst) 35 | // } 36 | -------------------------------------------------------------------------------- /server/sessdata/compress_test.go: -------------------------------------------------------------------------------- 1 | package sessdata 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestLzsCompress(t *testing.T) { 11 | var ( 12 | n int 13 | err error 14 | ) 15 | assert := assert.New(t) 16 | c := LzsgoCmp{} 17 | s := "hello anylink, you are best!" 18 | src := []byte(strings.Repeat(s, 50)) 19 | 20 | comprBuf := make([]byte, 2048) 21 | n, err = c.Compress(src, comprBuf) 22 | assert.Nil(err) 23 | 24 | unprBuf := make([]byte, 2048) 25 | n, err = c.Uncompress(comprBuf[:n], unprBuf) 26 | assert.Nil(err) 27 | assert.Equal(src, unprBuf[:n]) 28 | } 29 | -------------------------------------------------------------------------------- /server/sessdata/copy_struct.go: -------------------------------------------------------------------------------- 1 | package sessdata 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | // 用b的所有字段覆盖a的 9 | // 如果fields不为空, 表示用b的特定字段覆盖a的 10 | // a应该为结构体指针 11 | func CopyStruct(a interface{}, b interface{}, fields ...string) (err error) { 12 | at := reflect.TypeOf(a) 13 | av := reflect.ValueOf(a) 14 | bt := reflect.TypeOf(b) 15 | bv := reflect.ValueOf(b) 16 | 17 | // 简单判断下 18 | if at.Kind() != reflect.Ptr { 19 | err = fmt.Errorf("a must be a struct pointer") 20 | return 21 | } 22 | av = reflect.ValueOf(av.Interface()) 23 | 24 | // 要复制哪些字段 25 | _fields := make([]string, 0) 26 | if len(fields) > 0 { 27 | _fields = fields 28 | } else { 29 | for i := 0; i < bv.NumField(); i++ { 30 | _fields = append(_fields, bt.Field(i).Name) 31 | } 32 | } 33 | 34 | if len(_fields) == 0 { 35 | fmt.Println("no fields to copy") 36 | return 37 | } 38 | 39 | // 复制 40 | for i := 0; i < len(_fields); i++ { 41 | name := _fields[i] 42 | f := av.Elem().FieldByName(name) 43 | bValue := bv.FieldByName(name) 44 | 45 | // a中有同名的字段并且类型一致才复制 46 | if f.IsValid() && f.Kind() == bValue.Kind() { 47 | f.Set(bValue) 48 | } 49 | } 50 | return 51 | } 52 | -------------------------------------------------------------------------------- /server/sessdata/copy_struct_test.go: -------------------------------------------------------------------------------- 1 | package sessdata 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | type A struct { 10 | Id int 11 | Name string 12 | Age int 13 | Addr string 14 | } 15 | 16 | type B struct { 17 | IdB int 18 | NameB string 19 | Age int 20 | Addr string 21 | } 22 | 23 | func TestCopyStruct(t *testing.T) { 24 | assert := assert.New(t) 25 | a := A{ 26 | Id: 1, 27 | Name: "bob", 28 | Age: 15, 29 | Addr: "American", 30 | } 31 | b := B{} 32 | err := CopyStruct(&b, a) 33 | assert.Nil(err) 34 | assert.Equal(b.IdB, 0) 35 | assert.Equal(b.NameB, "") 36 | assert.Equal(b.Age, 15) 37 | assert.Equal(b.Addr, "American") 38 | } 39 | -------------------------------------------------------------------------------- /server/sessdata/ip_pool_test.go: -------------------------------------------------------------------------------- 1 | package sessdata 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | "path" 8 | "testing" 9 | "time" 10 | 11 | "github.com/bjdgyc/anylink/base" 12 | "github.com/bjdgyc/anylink/dbdata" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func preData(tmpDir string) { 17 | base.Test() 18 | tmpDb := path.Join(tmpDir, "test.db") 19 | base.Cfg.DbType = "sqlite3" 20 | base.Cfg.DbSource = tmpDb 21 | base.Cfg.Ipv4CIDR = "192.168.3.0/24" 22 | base.Cfg.Ipv4Gateway = "192.168.3.1" 23 | base.Cfg.Ipv4Start = "192.168.3.100" 24 | base.Cfg.Ipv4End = "192.168.3.150" 25 | base.Cfg.MaxClient = 100 26 | base.Cfg.MaxUserClient = 3 27 | base.Cfg.IpLease = 5 28 | 29 | dbdata.Start() 30 | group := dbdata.Group{ 31 | Name: "group1", 32 | Bandwidth: 1000, 33 | } 34 | _ = dbdata.Add(&group) 35 | initIpPool() 36 | } 37 | 38 | func cleardata(tmpDir string) { 39 | _ = dbdata.Stop() 40 | tmpDb := path.Join(tmpDir, "test.db") 41 | os.Remove(tmpDb) 42 | } 43 | 44 | func TestIpPool(t *testing.T) { 45 | assert := assert.New(t) 46 | tmp := t.TempDir() 47 | preData(tmp) 48 | defer cleardata(tmp) 49 | 50 | var ip net.IP 51 | 52 | for i := 100; i <= 150; i++ { 53 | _ = AcquireIp(getTestUser(i), getTestMacAddr(i), true) 54 | } 55 | 56 | // 回收 57 | ReleaseIp(net.IPv4(192, 168, 3, 140), getTestMacAddr(140)) 58 | time.Sleep(time.Second * 6) 59 | 60 | // 从头循环获取可用ip 61 | user_new := getTestUser(210) 62 | mac_new := getTestMacAddr(210) 63 | ip = AcquireIp(user_new, mac_new, true) 64 | t.Log("mac_new", ip) 65 | assert.NotNil(ip) 66 | assert.True(net.IPv4(192, 168, 3, 140).Equal(ip)) 67 | 68 | // 回收全部 69 | for i := 100; i <= 150; i++ { 70 | ReleaseIp(net.IPv4(192, 168, 3, byte(i)), getTestMacAddr(i)) 71 | } 72 | } 73 | 74 | func getTestUser(i int) string { 75 | return fmt.Sprintf("user-%d", i) 76 | } 77 | 78 | func getTestMacAddr(i int) string { 79 | // 前缀mac 80 | macAddr := "02:00:00:00:00" 81 | return fmt.Sprintf("%s:%x", macAddr, i) 82 | } 83 | -------------------------------------------------------------------------------- /server/sessdata/limit_client.go: -------------------------------------------------------------------------------- 1 | package sessdata 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/bjdgyc/anylink/base" 7 | ) 8 | 9 | const limitAllKey = "__ALL__" 10 | 11 | var ( 12 | limitClient = map[string]int{limitAllKey: 0} 13 | limitMux = sync.Mutex{} 14 | ) 15 | 16 | func LimitClient(user string, close bool) bool { 17 | limitMux.Lock() 18 | defer limitMux.Unlock() 19 | 20 | _all := limitClient[limitAllKey] 21 | c, ok := limitClient[user] 22 | if !ok { // 不存在用户 23 | limitClient[user] = 0 24 | } 25 | 26 | if close { 27 | limitClient[user] = c - 1 28 | limitClient[limitAllKey] = _all - 1 29 | return true 30 | } 31 | 32 | // 全局判断 33 | if _all >= base.Cfg.MaxClient { 34 | return false 35 | } 36 | 37 | // 超出同一个用户限制 38 | if c >= base.Cfg.MaxUserClient { 39 | return false 40 | } 41 | 42 | limitClient[user] = c + 1 43 | limitClient[limitAllKey] = _all + 1 44 | return true 45 | } 46 | -------------------------------------------------------------------------------- /server/sessdata/limit_rate.go: -------------------------------------------------------------------------------- 1 | package sessdata 2 | 3 | import ( 4 | "context" 5 | 6 | "golang.org/x/time/rate" 7 | ) 8 | 9 | type LimitRater struct { 10 | limit *rate.Limiter 11 | } 12 | 13 | // lim: 令牌产生速率 14 | // burst: 允许的最大爆发速率 15 | func NewLimitRater(lim, burst int) *LimitRater { 16 | limit := rate.NewLimiter(rate.Limit(lim), burst) 17 | return &LimitRater{limit: limit} 18 | } 19 | 20 | // bt 不能超过burst大小 21 | func (l *LimitRater) Wait(bt int) error { 22 | return l.limit.WaitN(context.Background(), bt) 23 | } 24 | -------------------------------------------------------------------------------- /server/sessdata/limit_test.go: -------------------------------------------------------------------------------- 1 | package sessdata 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/bjdgyc/anylink/base" 10 | ) 11 | 12 | // func TestCheckUser(t *testing.T) { 13 | // user["user1"] = User{Password: "7c4a8d09ca3762af61e59520943dc26494f8941b"} 14 | // user["user2"] = User{Password: "7c4a8d09ca3762af61e59520943dc26494f8941c"} 15 | // 16 | // var res bool 17 | // res = CheckUser("user1", "123456", "") 18 | // AssertTrue(t, res == true) 19 | // 20 | // res = CheckUser("user2", "123457", "") 21 | // AssertTrue(t, res == false) 22 | // } 23 | 24 | func TestLimitClient(t *testing.T) { 25 | assert := assert.New(t) 26 | base.Cfg.MaxClient = 2 27 | base.Cfg.MaxUserClient = 1 28 | 29 | res1 := LimitClient("user1", false) 30 | res2 := LimitClient("user1", false) 31 | res3 := LimitClient("user2", false) 32 | res4 := LimitClient("user3", false) 33 | res5 := LimitClient("user1", true) 34 | 35 | assert.True(res1) 36 | assert.False(res2) 37 | assert.True(res3) 38 | assert.False(res4) 39 | assert.True(res5) 40 | 41 | } 42 | 43 | func TestLimitWait(t *testing.T) { 44 | assert := assert.New(t) 45 | limit := NewLimitRater(1, 2) 46 | err := limit.Wait(2) 47 | assert.Nil(err) 48 | start := time.Now() 49 | err = limit.Wait(2) 50 | assert.Nil(err) 51 | err = limit.Wait(1) 52 | assert.Nil(err) 53 | end := time.Now() 54 | sub := end.Sub(start) 55 | assert.Equal(3, int(sub.Seconds())) 56 | } 57 | -------------------------------------------------------------------------------- /server/sessdata/online.go: -------------------------------------------------------------------------------- 1 | package sessdata 2 | 3 | import ( 4 | "bytes" 5 | "net" 6 | "sort" 7 | "strings" 8 | "time" 9 | 10 | "github.com/bjdgyc/anylink/pkg/utils" 11 | ) 12 | 13 | type Online struct { 14 | Token string `json:"token"` 15 | Username string `json:"username"` 16 | Group string `json:"group"` 17 | MacAddr string `json:"mac_addr"` 18 | UniqueMac bool `json:"unique_mac"` 19 | Ip net.IP `json:"ip"` 20 | RemoteAddr string `json:"remote_addr"` 21 | TransportProtocol string `json:"transport_protocol"` 22 | TunName string `json:"tun_name"` 23 | Mtu int `json:"mtu"` 24 | Client string `json:"client"` 25 | BandwidthUp string `json:"bandwidth_up"` 26 | BandwidthDown string `json:"bandwidth_down"` 27 | BandwidthUpAll string `json:"bandwidth_up_all"` 28 | BandwidthDownAll string `json:"bandwidth_down_all"` 29 | LastLogin time.Time `json:"last_login"` 30 | } 31 | 32 | type Onlines []Online 33 | 34 | func (o Onlines) Len() int { 35 | return len(o) 36 | } 37 | 38 | func (o Onlines) Less(i, j int) bool { 39 | return bytes.Compare(o[i].Ip, o[j].Ip) < 0 40 | } 41 | 42 | func (o Onlines) Swap(i, j int) { 43 | o[i], o[j] = o[j], o[i] 44 | } 45 | 46 | func OnlineSess() []Online { 47 | return GetOnlineSess("", "", false) 48 | } 49 | 50 | /** 51 | * @Description: GetOnlineSess 52 | * @param search_cate 分类:用户名、登录组、MAC地址、IP地址、远端地址 53 | * @param search_text 关键字,模糊搜索 54 | * @param show_sleeper 是否显示休眠用户 55 | * @return []Online 56 | */ 57 | func GetOnlineSess(search_cate string, search_text string, show_sleeper bool) []Online { 58 | var datas Onlines 59 | if strings.TrimSpace(search_text) == "" { 60 | search_cate = "" 61 | } 62 | sessMux.Lock() 63 | defer sessMux.Unlock() 64 | for _, v := range sessions { 65 | v.mux.Lock() 66 | cSess := v.CSess 67 | if cSess == nil { 68 | cSess = &ConnSession{} 69 | } 70 | // 选择需要比较的字符串 71 | var compareText string 72 | switch search_cate { 73 | case "username": 74 | compareText = v.Username 75 | case "group": 76 | compareText = v.Group 77 | case "mac_addr": 78 | compareText = v.MacAddr 79 | case "ip": 80 | if cSess != nil { 81 | compareText = cSess.IpAddr.String() 82 | } 83 | case "remote_addr": 84 | if cSess != nil { 85 | compareText = cSess.RemoteAddr 86 | } 87 | } 88 | if search_cate != "" && !strings.Contains(compareText, search_text) { 89 | v.mux.Unlock() 90 | continue 91 | } 92 | 93 | if show_sleeper || v.IsActive { 94 | transportProtocol := "TCP" 95 | dSess := cSess.GetDtlsSession() 96 | if dSess != nil { 97 | transportProtocol = "UDP" 98 | } 99 | val := Online{ 100 | Token: v.Token, 101 | Ip: cSess.IpAddr, 102 | Username: v.Username, 103 | Group: v.Group, 104 | MacAddr: v.MacAddr, 105 | UniqueMac: v.UniqueMac, 106 | RemoteAddr: cSess.RemoteAddr, 107 | TransportProtocol: transportProtocol, 108 | TunName: cSess.IfName, 109 | Mtu: cSess.Mtu, 110 | Client: cSess.Client, 111 | BandwidthUp: utils.HumanByte(cSess.BandwidthUpPeriod.Load()) + "/s", 112 | BandwidthDown: utils.HumanByte(cSess.BandwidthDownPeriod.Load()) + "/s", 113 | BandwidthUpAll: utils.HumanByte(cSess.BandwidthUpAll.Load()), 114 | BandwidthDownAll: utils.HumanByte(cSess.BandwidthDownAll.Load()), 115 | LastLogin: v.LastLogin, 116 | } 117 | datas = append(datas, val) 118 | } 119 | v.mux.Unlock() 120 | } 121 | sort.Sort(&datas) 122 | return datas 123 | } 124 | -------------------------------------------------------------------------------- /server/sessdata/protocol.go: -------------------------------------------------------------------------------- 1 | package sessdata 2 | 3 | type LType int8 4 | 5 | const ( 6 | LTypeEthernet LType = 1 7 | LTypeIPData LType = 2 8 | ) 9 | 10 | type Payload struct { 11 | LType LType // LinkType 12 | PType byte // payload types 13 | Data []byte 14 | } 15 | 16 | /* 17 | var header = []byte{'S', 'T', 'F', 0x01, 0, 0, 0x07, 0} 18 | https://tools.ietf.org/html/draft-mavrogiannopoulos-openconnect-02#section-2.2 19 | 20 | +---------------------+---------------------------------------------+ 21 | | byte | value | 22 | +---------------------+---------------------------------------------+ 23 | | 0 | fixed to 0x53 (S) | 24 | | | | 25 | | 1 | fixed to 0x54 (T) | 26 | | | | 27 | | 2 | fixed to 0x46 (F) | 28 | | | | 29 | | 3 | fixed to 0x01 | 30 | | | | 31 | | 4-5 | The length of the packet that follows this | 32 | | | header in big endian order | 33 | | | | 34 | | 6 | The type of the payload that follows (see | 35 | | | Table 3 for available types) | 36 | | | | 37 | | 7 | fixed to 0x00 | 38 | +---------------------+---------------------------------------------+ 39 | 40 | 41 | The available payload types are listed in Table 3. 42 | +---------------------+---------------------------------------------+ 43 | | Value | Description | 44 | +---------------------+---------------------------------------------+ 45 | | 0x00 | DATA: the TLS record packet contains an | 46 | | | IPv4 or IPv6 packet | 47 | | | | 48 | | 0x03 | DPD-REQ: used for dead peer detection. Once | 49 | | | sent the peer should reply with a DPD-RESP | 50 | | | packet, that has the same contents as the | 51 | | | original request. | 52 | | | | 53 | | 0x04 | DPD-RESP: used as a response to a | 54 | | | previously received DPD-REQ. | 55 | | | | 56 | | 0x05 | DISCONNECT: sent by the client (or server) | 57 | | | to terminate the session. No data is | 58 | | | associated with this request. The session | 59 | | | will be invalidated after such request. | 60 | | | | 61 | | 0x07 | KEEPALIVE: sent by any peer. No data is | 62 | | | associated with this request. | 63 | | | | 64 | | 0x08 | COMPRESSED DATA: a Data packet which is | 65 | | | compressed prior to encryption. | 66 | | | | 67 | | 0x09 | TERMINATE: sent by the server to indicate | 68 | | | that the server is shutting down. No data | 69 | | | is associated with this request. | 70 | +---------------------+---------------------------------------------+ 71 | */ 72 | -------------------------------------------------------------------------------- /server/sessdata/session_test.go: -------------------------------------------------------------------------------- 1 | package sessdata 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/bjdgyc/anylink/base" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestNewSession(t *testing.T) { 13 | ast := assert.New(t) 14 | sessions = make(map[string]*Session) 15 | sess := NewSession("") 16 | token := sess.Token 17 | v, ok := sessions[token] 18 | ast.True(ok) 19 | ast.Equal(sess, v) 20 | } 21 | 22 | func TestConnSession(t *testing.T) { 23 | ast := assert.New(t) 24 | tmp := t.TempDir() 25 | preData(tmp) 26 | defer cleardata(tmp) 27 | 28 | time.Sleep(time.Second * 10) 29 | 30 | sess := NewSession("") 31 | sess.Username = "user-test" 32 | sess.Group = "group1" 33 | sess.MacAddr = "00:15:5d:50:14:43" 34 | 35 | cSess := sess.NewConn() 36 | // base.Info("cSess", cSess) 37 | 38 | err := cSess.RateLimit(100, true) 39 | ast.Nil(err) 40 | ast.Equal(cSess.BandwidthUp.Load(), uint32(100)) 41 | err = cSess.RateLimit(200, false) 42 | ast.Nil(err) 43 | ast.Equal(cSess.BandwidthDown.Load(), uint32(200)) 44 | 45 | var ( 46 | cmpName string 47 | ok bool 48 | ) 49 | base.Cfg.Compression = true 50 | 51 | cmpName, ok = cSess.SetPickCmp("cstp", "oc-lz4,lzs") 52 | fmt.Println(cmpName, ok) 53 | ast.True(ok) 54 | ast.Equal(cmpName, "lzs") 55 | cmpName, ok = cSess.SetPickCmp("dtls", "lzs") 56 | ast.True(ok) 57 | ast.Equal(cmpName, "lzs") 58 | cmpName, ok = cSess.SetPickCmp("dtls", "test") 59 | ast.False(ok) 60 | ast.Equal(cmpName, "") 61 | 62 | cSess.Close() 63 | 64 | // 等待日志执行完成 65 | time.Sleep(time.Second * 10) 66 | } 67 | -------------------------------------------------------------------------------- /server/sessdata/start.go: -------------------------------------------------------------------------------- 1 | package sessdata 2 | 3 | func Start() { 4 | initIpPool() 5 | checkSession() 6 | saveStatsInfo() 7 | CloseUserLimittimeSession() 8 | } 9 | -------------------------------------------------------------------------------- /server/sessdata/statsinfo.go: -------------------------------------------------------------------------------- 1 | package sessdata 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/bjdgyc/anylink/dbdata" 8 | "github.com/shirou/gopsutil/cpu" 9 | "github.com/shirou/gopsutil/mem" 10 | ) 11 | 12 | const ( 13 | StatsCycleSec = 10 // 统计周期(秒) 14 | AddCycleSec = 60 // 记录到数据表周期(秒) 15 | ) 16 | 17 | func saveStatsInfo() { 18 | go func() { 19 | tick := time.NewTicker(time.Second * StatsCycleSec) 20 | count := 0 21 | for range tick.C { 22 | up := uint32(0) 23 | down := uint32(0) 24 | upGroups := make(map[int]uint32) 25 | downGroups := make(map[int]uint32) 26 | numGroups := make(map[int]int) 27 | onlineNum := 0 28 | sessMux.Lock() 29 | for _, v := range sessions { 30 | v.mux.Lock() 31 | if v.IsActive { 32 | // 在线人数 33 | onlineNum += 1 34 | numGroups[v.CSess.Group.Id] += 1 35 | // 网络吞吐 36 | userUp := v.CSess.BandwidthUpPeriod.Load() 37 | userDown := v.CSess.BandwidthDownPeriod.Load() 38 | if userUp > 0 { 39 | upGroups[v.CSess.Group.Id] += userUp 40 | } 41 | if userDown > 0 { 42 | downGroups[v.CSess.Group.Id] += userDown 43 | } 44 | up += userUp 45 | down += userDown 46 | } 47 | v.mux.Unlock() 48 | } 49 | sessMux.Unlock() 50 | 51 | tNow := time.Now() 52 | // online 53 | numData, _ := json.Marshal(numGroups) 54 | so := dbdata.StatsOnline{Num: onlineNum, NumGroups: string(numData), CreatedAt: tNow} 55 | // network 56 | upData, _ := json.Marshal(upGroups) 57 | downData, _ := json.Marshal(downGroups) 58 | sn := dbdata.StatsNetwork{Up: up, Down: down, UpGroups: string(upData), DownGroups: string(downData), CreatedAt: tNow} 59 | // cpu 60 | sc := dbdata.StatsCpu{Percent: getCpuPercent(), CreatedAt: tNow} 61 | // mem 62 | sm := dbdata.StatsMem{Percent: getMemPercent(), CreatedAt: tNow} 63 | count++ 64 | // 是否保存至数据库 65 | save := count*StatsCycleSec >= AddCycleSec 66 | // 历史数据 67 | if save { 68 | count = 0 69 | } 70 | // 设置统计数据 71 | setStatsData(save, so, sn, sc, sm) 72 | } 73 | }() 74 | } 75 | 76 | func setStatsData(save bool, so dbdata.StatsOnline, sn dbdata.StatsNetwork, sc dbdata.StatsCpu, sm dbdata.StatsMem) { 77 | // 实时数据 78 | dbdata.StatsInfoIns.SetRealTime("online", so) 79 | dbdata.StatsInfoIns.SetRealTime("network", sn) 80 | dbdata.StatsInfoIns.SetRealTime("cpu", sc) 81 | dbdata.StatsInfoIns.SetRealTime("mem", sm) 82 | if !save { 83 | return 84 | } 85 | dbdata.StatsInfoIns.SaveStatsInfo(so, sn, sc, sm) 86 | } 87 | 88 | func getCpuPercent() float64 { 89 | cpuUsedPercent, _ := cpu.Percent(0, false) 90 | percent := cpuUsedPercent[0] 91 | if percent == 0 { 92 | percent = 1 93 | } 94 | return decimal(percent) 95 | } 96 | 97 | func getMemPercent() float64 { 98 | m, _ := mem.VirtualMemory() 99 | return decimal(m.UsedPercent) 100 | } 101 | 102 | func decimal(f float64) float64 { 103 | i := int(f * 100) 104 | return float64(i) / 100 105 | } 106 | -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | 0.13.1 -------------------------------------------------------------------------------- /version_info: -------------------------------------------------------------------------------- 1 | version_info 2 | -------------------------------------------------------------------------------- /web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended' 9 | ], 10 | parserOptions: { 11 | parser: 'babel-eslint' 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /ui 5 | 6 | 7 | # local env files 8 | .env.local 9 | .env.*.local 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | pnpm-debug.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /web/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 bjdgyc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # anylink-web 2 | 3 | ## Repo 4 | 5 | > github: https://github.com/bjdgyc/anylink-web 6 | 7 | > gitee: https://gitee.com/bjdgyc/anylink-web 8 | 9 | ## Project setup 10 | ``` 11 | npm install 12 | ``` 13 | 14 | ### Compiles and hot-reloads for development 15 | ``` 16 | npm run serve 17 | ``` 18 | 19 | ### Compiles and minifies for production 20 | ``` 21 | npm run build 22 | ``` 23 | 24 | ### Lints and fixes files 25 | ``` 26 | npm run lint 27 | ``` 28 | 29 | ### Customize configuration 30 | See [Configuration Reference](https://cli.vuejs.org/config/). 31 | -------------------------------------------------------------------------------- /web/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anylink-web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.21.1", 12 | "core-js": "^3.6.5", 13 | "echarts": "^4.9.0", 14 | "element-ui": "^2.4.5", 15 | "qs": "^6.11.1", 16 | "vue": "^2.6.11", 17 | "vue-count-to": "^1.0.13", 18 | "vue-router": "^3.5.2", 19 | "vuedraggable": "^2.24.3" 20 | }, 21 | "devDependencies": { 22 | "@vue/cli-plugin-babel": "~4.5.0", 23 | "@vue/cli-plugin-eslint": "~4.5.0", 24 | "@vue/cli-service": "~4.5.0", 25 | "babel-eslint": "^10.1.0", 26 | "eslint": "^6.7.2", 27 | "eslint-plugin-vue": "^6.2.2", 28 | "vue-cli-plugin-element": "~1.0.1", 29 | "vue-template-compiler": "^2.6.11" 30 | }, 31 | "eslintConfig": { 32 | "root": true, 33 | "env": { 34 | "node": true 35 | }, 36 | "extends": [ 37 | "plugin:vue/essential", 38 | "eslint:recommended" 39 | ], 40 | "parserOptions": { 41 | "parser": "babel-eslint" 42 | }, 43 | "rules": {} 44 | }, 45 | "browserslist": [ 46 | "> 1%", 47 | "last 2 versions", 48 | "not dead" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjdgyc/anylink/805498e270a2172f116161f3534cfcf76de2f83d/web/public/favicon.ico -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | AnyLink 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /web/public/批量添加用户模版.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjdgyc/anylink/805498e270a2172f116161f3534cfcf76de2f83d/web/public/批量添加用户模版.xlsx -------------------------------------------------------------------------------- /web/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 37 | 38 | 85 | -------------------------------------------------------------------------------- /web/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjdgyc/anylink/805498e270a2172f116161f3534cfcf76de2f83d/web/src/assets/logo.png -------------------------------------------------------------------------------- /web/src/components/Cell.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | 22 | -------------------------------------------------------------------------------- /web/src/components/LineChart.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 122 | -------------------------------------------------------------------------------- /web/src/layout/Layout.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 63 | 64 | 89 | -------------------------------------------------------------------------------- /web/src/layout/LayoutAside.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 82 | 83 | 93 | -------------------------------------------------------------------------------- /web/src/layout/LayoutHeader.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 57 | 58 | 83 | -------------------------------------------------------------------------------- /web/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import './plugins/element' 4 | import "./plugins/mixin"; 5 | import request from './plugins/request' 6 | import router from "./plugins/router"; 7 | 8 | 9 | //TODO 10 | Vue.config.productionTip = false 11 | 12 | 13 | const vm = new Vue({ 14 | data: { 15 | // 判断是否登录 16 | isLogin: false, 17 | }, 18 | router, 19 | render: h => h(App), 20 | }).$mount('#app') 21 | 22 | request(vm) -------------------------------------------------------------------------------- /web/src/pages/Login.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 100 | 101 | -------------------------------------------------------------------------------- /web/src/pages/set/Audit.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 62 | -------------------------------------------------------------------------------- /web/src/pages/set/Soft.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 67 | 68 | 71 | -------------------------------------------------------------------------------- /web/src/plugins/element.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Element from 'element-ui' 3 | import 'element-ui/lib/theme-chalk/index.css' 4 | 5 | Vue.use(Element) 6 | -------------------------------------------------------------------------------- /web/src/plugins/mixin.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | 3 | function gDateFormat(p) { 4 | var da = new Date(p); 5 | var year = da.getFullYear(); 6 | var month = da.getMonth() + 1; 7 | var dt = da.getDate(); 8 | var h = ('0'+da.getHours()).slice(-2); 9 | var m = ('0'+da.getMinutes()).slice(-2) 10 | var s = ('0'+da.getSeconds()).slice(-2); 11 | 12 | return year + '-' + month + '-' + dt + ' ' + h + ':' + m + ':' + s; 13 | } 14 | 15 | var Mixin = { 16 | data() { 17 | return { 18 | user_edit_dialog: false, 19 | isLoading: false, 20 | } 21 | }, 22 | computed: {}, 23 | methods: { 24 | tableDateFormat(row, column) { 25 | var p = row[column.property]; 26 | if (p === undefined) { 27 | return ""; 28 | } 29 | return gDateFormat(p); 30 | }, 31 | tableArrayFormat(row, column) { 32 | var p = row[column.property]; 33 | if (p === undefined) { 34 | return ""; 35 | } 36 | return p.join("\n\r\n"); 37 | }, 38 | disVisible() { 39 | this.user_edit_dialog = false 40 | }, 41 | }, 42 | } 43 | 44 | // Vue.filter("dateFormat", function (p) { 45 | // return gDateFormat(p); 46 | // }) 47 | Vue.mixin(Mixin) 48 | 49 | 50 | // export default Mixin 51 | 52 | -------------------------------------------------------------------------------- /web/src/plugins/request.js: -------------------------------------------------------------------------------- 1 | // http://www.zhangwj.com/ 2 | // 全局的 axios 默认值 3 | import axios from "axios"; 4 | import {getToken, removeToken} from "./token"; 5 | // axios.defaults.headers.common['Jwt'] = AUTH_TOKEN; 6 | axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'; 7 | 8 | if (process.env.NODE_ENV !== 'production') { 9 | // 开发环境 10 | axios.defaults.baseURL = 'https://192.168.8.24:8800'; 11 | } 12 | 13 | function request(vm) { 14 | // HTTP 请求拦截器 15 | axios.interceptors.request.use(config => { 16 | // 在发送请求之前做些什么 17 | // 获取token, 并添加到 headers 请求头中 18 | const token = getToken(); 19 | if (token) { 20 | config.headers.Jwt = token; 21 | } 22 | return config; 23 | }); 24 | 25 | console.log(vm) 26 | 27 | // HTTP 响应拦截器 28 | // 统一处理 401 状态,token 过期的处理,清除token跳转login 29 | // 参数 1, 表示成功响应 30 | axios.interceptors.response.use(null, err => { 31 | // 没有登录或令牌过期 32 | if (err.response.status === 401) { 33 | // 注销,情况状态和token 34 | // vm.$store.dispatch("logout"); 35 | // 跳转的登录页 36 | removeToken(); 37 | vm.$router.push('/login'); 38 | // 注意: 这里的 vm 实例需要外部传入 39 | } 40 | return Promise.reject(err); 41 | }); 42 | } 43 | 44 | export default request 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /web/src/plugins/router.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueRouter from "vue-router"; 3 | import { getToken } from "./token"; 4 | 5 | Vue.use(VueRouter) 6 | 7 | 8 | const routes = [ 9 | { path: '/login', component: () => import('@/pages/Login') }, 10 | { 11 | path: '/admin', 12 | component: () => import('@/layout/Layout'), 13 | redirect: '/admin/home', 14 | children: [ 15 | { path: 'home', component: () => import('@/pages/Home') }, 16 | 17 | { path: 'set/system', component: () => import('@/pages/set/System') }, 18 | { path: 'set/soft', component: () => import('@/pages/set/Soft') }, 19 | { path: 'set/other', component: () => import('@/pages/set/Other') }, 20 | { path: 'set/audit', component: () => import('@/pages/set/Audit') }, 21 | 22 | { path: 'user/list', component: () => import('@/pages/user/List') }, 23 | { path: 'user/policy', component: () => import('@/pages/user/Policy') }, 24 | { path: 'user/online', component: () => import('@/pages/user/Online') }, 25 | { path: 'user/ip_map', component: () => import('@/pages/user/IpMap') }, 26 | { path: 'user/lockmanager', component: () => import('@/pages/user/LockManager') }, 27 | 28 | { path: 'group/list', component: () => import('@/pages/group/List') }, 29 | 30 | ], 31 | }, 32 | 33 | { path: '*', redirect: '/admin/home' }, 34 | ] 35 | 36 | // 3. 创建 router 实例,然后传 `routes` 配置 37 | // 你还可以传别的配置参数, 不过先这么简单着吧。 38 | const router = new VueRouter({ 39 | routes 40 | }) 41 | 42 | // 路由守卫 43 | router.beforeEach((to, from, next) => { 44 | // 判断要进入的路由是否需要认证 45 | 46 | const token = getToken(); 47 | 48 | console.log("beforeEach", from.path, to.path, token) 49 | // console.log(from) 50 | 51 | // 没有token,全都跳转到login 52 | if (!token) { 53 | if (to.path === "/login") { 54 | next(); 55 | return; 56 | } 57 | 58 | next({ 59 | path: '/login', 60 | query: { 61 | redirect: to.path 62 | } 63 | }); 64 | return; 65 | } 66 | 67 | if (to.path === "/login") { 68 | next({ path: '/admin/home' }); 69 | return; 70 | } 71 | 72 | // 有token情况下 73 | next(); 74 | }); 75 | 76 | export default router; 77 | 78 | -------------------------------------------------------------------------------- /web/src/plugins/token.js: -------------------------------------------------------------------------------- 1 | const tokenKey = 'AnyLink-Jwt-Token' 2 | const tokenUser = 'AnyLink-Jwt-User' 3 | 4 | export function getToken() { 5 | return localStorage.getItem(tokenKey) 6 | } 7 | 8 | export function setToken(token) { 9 | return localStorage.setItem(tokenKey, token) 10 | } 11 | 12 | export function setUser(username) { 13 | return localStorage.setItem(tokenUser, username) 14 | } 15 | 16 | export function getUser() { 17 | return localStorage.getItem(tokenUser) 18 | } 19 | 20 | export function removeToken() { 21 | return localStorage.removeItem(tokenKey) 22 | } 23 | -------------------------------------------------------------------------------- /web/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: '/ui', 3 | outputDir: './ui', 4 | productionSourceMap: false, //生产环境生成map 5 | } 6 | --------------------------------------------------------------------------------