├── .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 |
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 |
68 |
69 |
70 | 什么是 AnyLink?
71 | AnyLink 是一款面向企业级的远程办公 SSL VPN 软件,支持多人同时在线使用。它提供安全、便捷的访问内部网络资源的方式,使远程工作者能够有效协作。
72 |
73 | 核心功能
74 |
75 | - 安全远程访问:AnyLink 使用 SSL/TLS 加密技术,确保远程用户与企业网络之间的连接安全可靠。
76 | - 多用户支持:多个用户可以同时连接 VPN,实现不同地点团队的无缝协作。
77 | - 灵活网络访问:AnyLink 能够安全地让远程工作者访问内部资源,如文件、应用程序和数据库。
78 | - 集中化管理:该 VPN 解决方案提供集中化管理控制台,便于用户管理、访问控制和监控。
79 |
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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 |
{{ left }}
5 |
{{ right }}
6 |
7 |
8 |
9 |
10 |
11 |
21 |
22 |
--------------------------------------------------------------------------------
/web/src/components/LineChart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
122 |
--------------------------------------------------------------------------------
/web/src/layout/Layout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | Powered by AnyLink
25 |
26 | 企业级远程办公系统 AGPL-3.0 ⓒ 2020-present
27 |
28 |
29 |
30 |
31 |
32 |
33 |
63 |
64 |
89 |
--------------------------------------------------------------------------------
/web/src/layout/LayoutAside.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
68 |
69 |
70 |
71 |
82 |
83 |
93 |
--------------------------------------------------------------------------------
/web/src/layout/LayoutHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
22 |
23 |
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 |
2 |
3 |
4 |
5 |
6 | AnyLink SSL VPN管理后台
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | 登录
17 | 重置
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
100 |
101 |
--------------------------------------------------------------------------------
/web/src/pages/set/Audit.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
62 |
--------------------------------------------------------------------------------
/web/src/pages/set/Soft.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
11 |
12 |
13 |
17 |
18 |
19 |
23 |
24 |
25 |
28 |
29 | {{ scope.row.data }}
30 |
31 |
32 |
33 |
34 |
35 |
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 |
--------------------------------------------------------------------------------