├── .github
└── workflows
│ ├── ci.yml
│ ├── release-setup.yml
│ └── release.yml
├── .gitignore
├── .http
└── api.http
├── .idea
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── modules.xml
└── sslcon.iml
├── .run
├── go build sslcon.go.run.xml
├── go build vpnagent.go.exe.run.xml
└── go build vpnagent.go.run.xml
├── LICENSE
├── README.md
├── auth
└── auth.go
├── base
├── config.go
├── log.go
└── setup.go
├── cmd
├── connect.go
├── disconnect.go
├── jsonrpc.go
├── root.go
└── status.go
├── go.mod
├── go.sum
├── proto
├── dtd.go
└── protocol.go
├── rpc
├── connect.go
└── rpc.go
├── session
└── session.go
├── sslcon.go
├── svc
└── service.go
├── tun
├── rwcancel
│ └── rwcancel.go
├── tun.go
├── tun_darwin.go
├── tun_linux.go
└── tun_windows.go
├── utils
├── record.go
├── utils.go
├── vpnc
│ ├── vpnc_darwin.go
│ ├── vpnc_linux.go
│ └── vpnc_windows.go
└── waterutil
│ ├── ip_protocols.go
│ └── utils_ipv4.go
├── vpn
├── buffer.go
├── dtls.go
├── tls.go
├── tun.go
└── tunnel.go
└── vpnagent.go
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a golang project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
3 |
4 | name: CI
5 |
6 | on:
7 | push:
8 | branches: [ "main" ]
9 | pull_request:
10 | branches: [ "main" ]
11 |
12 | jobs:
13 |
14 | build:
15 | name: Build on ${{ matrix.os }}
16 | runs-on: ${{ matrix.os }}
17 | strategy:
18 | matrix:
19 | # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners
20 | os: [ubuntu-20.04, windows-2019, macos-14]
21 | steps:
22 | - uses: actions/checkout@v4
23 | with:
24 | submodules: 'recursive'
25 | - name: Set up Go
26 | uses: actions/setup-go@v5
27 | with:
28 | go-version: 'stable'
29 | - name: Install dependencies
30 | run: go get .
31 | - name: Build
32 | shell: bash
33 | run: |
34 | if [ "${{ matrix.os }}" = "ubuntu-20.04" ]; then
35 | go build -trimpath -ldflags "-s -w" -o vpnagent vpnagent.go
36 | go build -trimpath -ldflags "-s -w" -o sslcon sslcon.go
37 | elif [ "${{ matrix.os }}" = "windows-2019" ]; then
38 | go build -trimpath -ldflags "-s -w" -o vpnagent.exe vpnagent.go
39 | go build -trimpath -ldflags "-s -w" -o sslcon.exe sslcon.go
40 | elif [ "${{ matrix.os }}" = "macos-14" ]; then
41 | go build -trimpath -ldflags "-s -w" -o vpnagent vpnagent.go
42 | go build -trimpath -ldflags "-s -w" -o sslcon sslcon.go
43 | fi
44 |
--------------------------------------------------------------------------------
/.github/workflows/release-setup.yml:
--------------------------------------------------------------------------------
1 | name: Release Setup
2 |
3 | on:
4 | workflow_call
5 |
6 | jobs:
7 | release:
8 | name: "Continuous Release"
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Job info
12 | run: |
13 | echo "GitHub Ref: ${{ github.ref }}"
14 | - name: Delete old workflow runs
15 | uses: Mattraks/delete-workflow-runs@main
16 | with:
17 | retain_days: 2
18 | keep_minimum_runs: 2
19 | - name: Automatic release
20 | uses: "marvinpinto/action-automatic-releases@latest"
21 | if: startsWith(github.ref, 'refs/heads/')
22 | with:
23 | repo_token: "${{ secrets.GITHUB_TOKEN }}"
24 | automatic_release_tag: "continuous"
25 | prerelease: true
26 | title: "Continuous release"
27 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a golang project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
3 |
4 | name: Release
5 |
6 | on:
7 | workflow_dispatch
8 |
9 | jobs:
10 | setup:
11 | name: Setup
12 | uses: ./.github/workflows/release-setup.yml
13 | build:
14 | name: Build
15 | runs-on: ${{ matrix.os }}
16 | strategy:
17 | matrix:
18 | build: [linux, linux-riscv64, linux-arm64, linux-armv7, linux-mipsle, windows, windows-arm64, macos, macos-arm64]
19 | include:
20 | - build: linux
21 | os: ubuntu-20.04
22 | go: 'stable'
23 | archive-name: sslcon-linux-amd64.tar.gz
24 | - build: linux-riscv64
25 | os: ubuntu-20.04
26 | go: 'stable'
27 | archive-name: sslcon-linux-riscv64.tar.gz
28 | - build: linux-arm64
29 | os: ubuntu-20.04
30 | go: 'stable'
31 | archive-name: sslcon-linux-arm64.tar.gz
32 | - build: linux-armv7
33 | os: ubuntu-20.04
34 | go: 'stable'
35 | archive-name: sslcon-linux-armv7.tar.gz
36 | - build: linux-mipsle
37 | os: ubuntu-20.04
38 | go: 'stable'
39 | archive-name: sslcon-linux-mipsle.tar.gz
40 | - build: windows
41 | os: windows-2019
42 | go: 'stable'
43 | archive-name: sslcon-windows10-amd64.7z
44 | - build: windows-arm64
45 | os: windows-2019
46 | go: 'stable'
47 | archive-name: sslcon-windows10-arm64.7z
48 | - build: macos
49 | os: macos-12
50 | go: 'stable'
51 | archive-name: sslcon-macOS-amd64.tar.gz
52 | - build: macos-arm64
53 | os: macos-14
54 | go: 'stable'
55 | archive-name: sslcon-macOS-arm64.tar.gz
56 | steps:
57 | - uses: actions/checkout@v4
58 | with:
59 | submodules: 'recursive'
60 | - name: Set up Go
61 | uses: actions/setup-go@v5
62 | with:
63 | go-version: ${{ matrix.go }}
64 | - name: Install dependencies
65 | run: go get .
66 | - name: Build
67 | shell: bash
68 | run: |
69 | if [ "${{ matrix.build }}" = "linux" ]; then
70 | go build -trimpath -ldflags "-s -w" -o vpnagent vpnagent.go
71 | go build -trimpath -ldflags "-s -w" -o sslcon sslcon.go
72 | elif [ "${{ matrix.build }}" = "linux-riscv64" ]; then
73 | GOOS=linux GOARCH=riscv64 go build -trimpath -ldflags "-s -w" -o vpnagent vpnagent.go
74 | GOOS=linux GOARCH=riscv64 go build -trimpath -ldflags "-s -w" -o sslcon sslcon.go
75 | elif [ "${{ matrix.build }}" = "linux-arm64" ]; then
76 | GOOS=linux GOARCH=arm64 go build -trimpath -ldflags "-s -w" -o vpnagent vpnagent.go
77 | GOOS=linux GOARCH=arm64 go build -trimpath -ldflags "-s -w" -o sslcon sslcon.go
78 | elif [ "${{ matrix.build }}" = "linux-armv7" ]; then
79 | GOOS=linux GOARM=7 GOARCH=arm go build -trimpath -ldflags "-s -w" -o vpnagent vpnagent.go
80 | GOOS=linux GOARM=7 GOARCH=arm go build -trimpath -ldflags "-s -w" -o sslcon sslcon.go
81 | elif [ "${{ matrix.build }}" = "linux-mipsle" ]; then
82 | GOOS=linux GOARCH=mipsle go build -trimpath -ldflags "-s -w" -o vpnagent vpnagent.go
83 | GOOS=linux GOARCH=mipsle go build -trimpath -ldflags "-s -w" -o sslcon sslcon.go
84 | elif [ "${{ matrix.build }}" = "windows" ]; then
85 | go build -trimpath -ldflags "-s -w" -o vpnagent.exe vpnagent.go
86 | go build -trimpath -ldflags "-s -w" -o sslcon.exe sslcon.go
87 | elif [ "${{ matrix.build }}" = "windows-arm64" ]; then
88 | GOARCH=arm64 go build -trimpath -ldflags "-s -w" -o vpnagent.exe vpnagent.go
89 | GOARCH=arm64 go build -trimpath -ldflags "-s -w" -o sslcon.exe sslcon.go
90 | elif [ "${{ matrix.build }}" = "macos" ]; then
91 | go build -trimpath -ldflags "-s -w" -o vpnagent vpnagent.go
92 | go build -trimpath -ldflags "-s -w" -o sslcon sslcon.go
93 | elif [ "${{ matrix.build }}" = "macos-arm64" ]; then
94 | go build -trimpath -ldflags "-s -w" -o vpnagent vpnagent.go
95 | go build -trimpath -ldflags "-s -w" -o sslcon sslcon.go
96 | fi
97 | - name: Build archive
98 | shell: bash
99 | run: |
100 | mkdir archive
101 | cp LICENSE README.md archive/
102 | # ls -lR
103 | if [ "${{ matrix.build }}" = "windows" -o "${{ matrix.build }}" = "windows-arm64" ]; then
104 | cp vpnagent.exe sslcon.exe ./archive/
105 | cd archive
106 | 7z a "${{ matrix.archive-name }}" LICENSE README.md vpnagent.exe sslcon.exe
107 | else
108 | cp vpnagent sslcon ./archive/
109 | cd archive
110 | tar -czf "${{ matrix.archive-name }}" LICENSE README.md vpnagent sslcon
111 | fi
112 | - name: Continuous release
113 | uses: softprops/action-gh-release@v1
114 | if: startsWith(github.ref, 'refs/heads/')
115 | with:
116 | prerelease: false
117 | files: archive/${{ matrix.archive-name }}
118 | tag_name: continuous
119 |
120 | - if: startsWith(github.ref, 'refs/tags/')
121 | name: Tagged release
122 | uses: softprops/action-gh-release@v1
123 | with:
124 | files: archive/${{ matrix.archive-name }}
125 | name: Release build (${{ github.ref_name }})
126 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/*
2 | !.idea/codeStyles
3 | !.idea/*.iml
4 | !.idea/modules.xml
5 | .http/http-client.private.env.json
6 | .DS_Store
7 |
8 | test.go
9 | test
10 | vpnagent
11 | sslcon
12 | *.exe
--------------------------------------------------------------------------------
/.http/api.http:
--------------------------------------------------------------------------------
1 | ### connect
2 | // It is possible to specify client messages in request body. Use '===' to separate messages.
3 | // Add '=== wait-for-server' above a message to send it after a server response is received.
4 | // To wait for N responses, add '=== wait-for-server' N times.
5 | WEBSOCKET ws://127.0.0.1:6210/rpc
6 | Content-Type: application/json // We use it for highlighting
7 |
8 | ===
9 | {
10 | "jsonrpc": "2.0",
11 | "method": "connect",
12 | "params": {
13 | "host": "{{host}}",
14 | "username": "{{username}}",
15 | "password": "{{password}}",
16 | "group": "{{group}}",
17 | "secret": "{{secret}}"
18 | },
19 | "id": 2
20 | }
21 |
22 | === wait-for-server
23 | {
24 | "jsonrpc": "2.0",
25 | "method": "status",
26 | "id": 0
27 | }
28 |
29 | ### disconnect
30 | WEBSOCKET ws://127.0.0.1:6210/rpc
31 | Content-Type: application/json
32 |
33 | ===
34 | {
35 | "jsonrpc": "2.0",
36 | "method": "disconnect",
37 | "id": 3
38 | }
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/sslcon.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.run/go build sslcon.go.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.run/go build vpnagent.go.exe.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.run/go build vpnagent.go.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 TLSLink
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## sslcon
4 |
5 | This is a Golang implementation of the [OpenConnect VPN Protocol](https://datatracker.ietf.org/doc/html/draft-mavrogiannopoulos-openconnect-04) for client side development.
6 |
7 | The released binaries contain a command line program(sslcon) and a VPN service agent(vpnagent), the latter of which should be run as a separate background service with root privileges, so that the front-end UI does not require an administrator authorization every time it starts.
8 |
9 | The API is exposed through the WebSocket and JSON-RPC 2.0 protocols, so developers can easily customize a graphical interface that meets their needs.
10 |
11 | **[There](https://github.com/tlslink/anylink-client) is a GUI client example showing how to use this project.**
12 |
13 | Currently the following servers are supported,
14 |
15 | - [AnyLink](https://github.com/bjdgyc/anylink)
16 | - [OpenConnect VPN server](https://gitlab.com/openconnect/ocserv)
17 |
18 | ## CLI
19 |
20 | ```
21 | $ ./sslcon
22 | A CLI application that supports the OpenConnect SSL VPN protocol.
23 | For more information, please visit https://github.com/tlslink/sslcon
24 |
25 | Usage:
26 | sslcon [flags]
27 | sslcon [command]
28 |
29 | Available Commands:
30 | connect Connect to the VPN server
31 | disconnect Disconnect from the VPN server
32 | status Get VPN connection information
33 |
34 | Flags:
35 | -h, --help help for sslcon
36 |
37 | Use "sslcon [command] --help" for more information about a command.
38 | ```
39 |
40 | ### install
41 |
42 | ```shell
43 | sudo ./vpnagent install
44 | # uninstall
45 | sudo ./vpnagent uninstall
46 | ```
47 | the installed service on systemd linux
48 |
49 | ```
50 | sudo systemctl stop/start/restart sslcon.service
51 | sudo systemctl disable/enable sslcon.service
52 | ```
53 |
54 | the installed service on OpenWrt
55 |
56 | ```
57 | /etc/init.d/sslcon stop/start/restart/status
58 | ```
59 |
60 | ### connect
61 |
62 | ```bash
63 | ./sslcon connect -s test.com -u vpn -g default -k key
64 | ```
65 |
66 | ### disconnect
67 |
68 | ```
69 | ./sslcon disconnect
70 | ```
71 |
72 | ### status
73 |
74 | ```
75 | ./sslcon status
76 | ```
77 |
78 | ## APIs
79 |
80 | You can use any WebSocket tool to test the API.
81 |
82 | ws://127.0.0.1:6210/rpc
83 |
84 | ### status
85 |
86 | ```json
87 | {
88 | "jsonrpc": "2.0",
89 | "method": "status",
90 | "id": 0
91 | }
92 | ```
93 |
94 | ### config
95 |
96 | ```json
97 | {
98 | "jsonrpc": "2.0",
99 | "method": "config",
100 | "params": {
101 | "log_level": "Debug",
102 | "log_path": ""
103 | },
104 | "id": 1
105 | }
106 | ```
107 |
108 | ### connect
109 |
110 | ```json
111 | {
112 | "jsonrpc": "2.0",
113 | "method": "connect",
114 | "params": {
115 | "host": "vpn.test.com",
116 | "username": "vpn",
117 | "password": "123456",
118 | "group": "",
119 | "secret": ""
120 | },
121 | "id": 2
122 | }
123 | ```
124 |
125 | ### disconnect
126 |
127 | ```json
128 | {
129 | "jsonrpc": "2.0",
130 | "method": "disconnect",
131 | "id": 3
132 | }
133 | ```
134 |
135 | ### reconnect
136 |
137 | ```json
138 | {
139 | "jsonrpc": "2.0",
140 | "method": "reconnect",
141 | "id": 4
142 | }
143 | ```
144 |
145 | ### stat
146 |
147 | ```json
148 | {
149 | "jsonrpc": "2.0",
150 | "method": "stat",
151 | "id": 7
152 | }
153 | ```
154 |
--------------------------------------------------------------------------------
/auth/auth.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "crypto/tls"
7 | "encoding/xml"
8 | "errors"
9 | "fmt"
10 | "io"
11 | "net"
12 | "net/http"
13 | "runtime"
14 | "strings"
15 | "text/template"
16 | "time"
17 |
18 | "github.com/elastic/go-sysinfo"
19 | "sslcon/base"
20 | "sslcon/proto"
21 | "sslcon/session"
22 | "sslcon/utils"
23 | )
24 |
25 | var (
26 | Prof = &Profile{Initialized: false}
27 | Conn *tls.Conn // tls.Conn 是结构体,net.Conn 是接口,所以这里可以用指针类型
28 | BufR *bufio.Reader
29 | reqHeaders = make(map[string]string)
30 | WebVpnCookie string
31 | )
32 |
33 | // Profile 模板变量字段必须导出,虽然全局,但每次连接都被重置
34 | type Profile struct {
35 | Host string `json:"host"`
36 | Username string `json:"username"`
37 | Password string `json:"password"`
38 | Group string `json:"group"`
39 | SecretKey string `json:"secret"`
40 |
41 | Initialized bool
42 | AppVersion string // for report to server in xml
43 |
44 | HostWithPort string
45 | Scheme string
46 | AuthPath string
47 |
48 | MacAddress string
49 | TunnelGroup string
50 | GroupAlias string
51 | ConfigHash string
52 |
53 | ComputerName string
54 | DeviceType string
55 | PlatformVersion string
56 | UniqueId string
57 | }
58 |
59 | const (
60 | tplInit = iota
61 | tplAuthReply
62 | )
63 |
64 | func init() {
65 | reqHeaders["X-Transcend-Version"] = "1"
66 | reqHeaders["X-Aggregate-Auth"] = "1"
67 |
68 | Prof.Scheme = "https://"
69 |
70 | host, _ := sysinfo.Host()
71 | info := host.Info()
72 | Prof.ComputerName = info.Hostname
73 | Prof.UniqueId = info.UniqueID
74 |
75 | os := info.OS
76 | Prof.DeviceType = os.Name
77 | if runtime.GOOS == "windows" {
78 | Prof.PlatformVersion = os.Build
79 | } else {
80 | Prof.PlatformVersion = strings.Split(os.Version, " ")[0]
81 | }
82 | // log.Printf("%+v %+v", info, os)
83 | }
84 |
85 | // InitAuth 确定用户组和服务端认证地址 AuthPath
86 | func InitAuth() error {
87 | WebVpnCookie = ""
88 | // https://github.com/mwitkow/go-http-dialer
89 | config := tls.Config{
90 | InsecureSkipVerify: base.Cfg.InsecureSkipVerify,
91 | }
92 | var err error
93 | Conn, err = tls.DialWithDialer(&net.Dialer{Timeout: 6 * time.Second}, "tcp4", Prof.HostWithPort, &config)
94 | if err != nil {
95 | return err
96 | }
97 | BufR = bufio.NewReader(Conn)
98 | // base.Info(Conn.ConnectionState().Version)
99 |
100 | dtd := new(proto.DTD)
101 |
102 | Prof.AppVersion = base.Cfg.AgentVersion
103 | Prof.MacAddress = base.LocalInterface.Mac
104 |
105 | err = tplPost(tplInit, "", dtd)
106 | if err != nil {
107 | return err
108 | }
109 | Prof.AuthPath = dtd.Auth.Form.Action
110 | if Prof.AuthPath == "" {
111 | Prof.AuthPath = "/"
112 | }
113 | Prof.TunnelGroup = dtd.Opaque.TunnelGroup
114 | Prof.GroupAlias = dtd.Opaque.GroupAlias
115 | Prof.ConfigHash = dtd.Opaque.ConfigHash
116 |
117 | gps := len(dtd.Auth.Form.Groups)
118 | if gps != 0 && !utils.InArray(dtd.Auth.Form.Groups, Prof.Group) {
119 | return fmt.Errorf("available user groups are: %s", strings.Join(dtd.Auth.Form.Groups, " "))
120 | }
121 |
122 | return nil
123 | }
124 |
125 | // PasswordAuth 认证成功后,服务端新建 ConnSession,并生成 SessionToken 或者通过 Header 返回 WebVpnCookie
126 | func PasswordAuth() error {
127 | dtd := new(proto.DTD)
128 | // 发送用户名或者用户名+密码
129 | err := tplPost(tplAuthReply, Prof.AuthPath, dtd)
130 | if err != nil {
131 | return err
132 | }
133 | // 兼容两步登陆,如必要则再次发送
134 | if dtd.Type == "auth-request" && dtd.Auth.Error.Value == "" {
135 | dtd = new(proto.DTD)
136 | err = tplPost(tplAuthReply, Prof.AuthPath, dtd)
137 | if err != nil {
138 | return err
139 | }
140 | }
141 | // 用户名、密码等错误
142 | if dtd.Type == "auth-request" {
143 | if dtd.Auth.Error.Value != "" {
144 | return fmt.Errorf(dtd.Auth.Error.Value, dtd.Auth.Error.Param1)
145 | }
146 | return errors.New(dtd.Auth.Message)
147 | }
148 |
149 | // AnyConnect 客户端支持 XML,OpenConnect 不使用 XML,而是使用 Cookie 反馈给客户端登陆状态
150 | session.Sess.SessionToken = dtd.SessionToken
151 | // 兼容 OpenConnect
152 | if WebVpnCookie != "" {
153 | session.Sess.SessionToken = WebVpnCookie
154 | }
155 | base.Debug("SessionToken:" + session.Sess.SessionToken)
156 | return nil
157 | }
158 |
159 | // 渲染模板并发送请求
160 | func tplPost(typ int, path string, dtd *proto.DTD) error {
161 | tplBuffer := new(bytes.Buffer)
162 | if typ == tplInit {
163 | t, _ := template.New("init").Parse(templateInit)
164 | _ = t.Execute(tplBuffer, Prof)
165 | } else {
166 | t, _ := template.New("auth_reply").Parse(templateAuthReply)
167 | _ = t.Execute(tplBuffer, Prof)
168 | }
169 | if base.Cfg.LogLevel == "Debug" {
170 | post := tplBuffer.String()
171 | if typ == tplAuthReply {
172 | post = utils.RemoveBetween(post, "", "")
173 | }
174 | base.Debug(post)
175 | }
176 | url := fmt.Sprintf("%s%s%s", Prof.Scheme, Prof.HostWithPort, path)
177 | if Prof.SecretKey != "" {
178 | url += "?" + Prof.SecretKey
179 | }
180 | req, _ := http.NewRequest("POST", url, tplBuffer)
181 |
182 | utils.SetCommonHeader(req)
183 | for k, v := range reqHeaders {
184 | req.Header[k] = []string{v}
185 | }
186 |
187 | err := req.Write(Conn)
188 | if err != nil {
189 | Conn.Close()
190 | return err
191 | }
192 |
193 | var resp *http.Response
194 | resp, err = http.ReadResponse(BufR, req)
195 | if err != nil {
196 | Conn.Close()
197 | return err
198 | }
199 | defer resp.Body.Close()
200 |
201 | body, err := io.ReadAll(resp.Body)
202 | if err != nil {
203 | Conn.Close()
204 | return err
205 | }
206 | if base.Cfg.LogLevel == "Debug" {
207 | base.Debug(string(body))
208 | }
209 |
210 | if resp.StatusCode == http.StatusOK {
211 | err = xml.Unmarshal(body, dtd)
212 | if dtd.Type == "complete" && dtd.SessionToken == "" {
213 | // 兼容 ocserv
214 | cookies := resp.Cookies()
215 | if len(cookies) != 0 {
216 | for _, c := range cookies {
217 | if c.Name == "webvpn" {
218 | WebVpnCookie = c.Value
219 | break
220 | }
221 | }
222 | }
223 | }
224 | // nil
225 | return err
226 | }
227 | Conn.Close()
228 | return fmt.Errorf("auth error %s", resp.Status)
229 | }
230 |
231 | var templateInit = `
232 |
233 | {{.AppVersion}}
234 |
235 | `
236 |
237 | // https://datatracker.ietf.org/doc/html/draft-mavrogiannopoulos-openconnect-03#section-2.1.2.2
238 | var templateAuthReply = `
239 |
240 | {{.AppVersion}}
241 |
242 |
243 | {{.TunnelGroup}}
244 | {{.GroupAlias}}
245 | {{.ConfigHash}}
246 |
247 |
248 | {{.MacAddress}}
249 |
250 |
251 | {{.Username}}
252 | {{.Password}}
253 |
254 | {{.Group}}
255 | `
256 |
--------------------------------------------------------------------------------
/base/config.go:
--------------------------------------------------------------------------------
1 | package base
2 |
3 | var (
4 | Cfg = &ClientConfig{}
5 | LocalInterface = &Interface{}
6 | )
7 |
8 | type ClientConfig struct {
9 | LogLevel string `json:"log_level"`
10 | LogPath string `json:"log_path"`
11 | InsecureSkipVerify bool `json:"skip_verify"`
12 | CiscoCompat bool `json:"cisco_compat"`
13 | NoDTLS bool `json:"no_dtls"`
14 | AgentName string `json:"agent_name"`
15 | AgentVersion string `json:"agent_version"`
16 | }
17 |
18 | // Interface 应该由外部接口设置
19 | type Interface struct {
20 | Name string `json:"name"`
21 | Ip4 string `json:"ip4"`
22 | Mac string `json:"mac"`
23 | Gateway string `json:"gateway"`
24 | }
25 |
26 | func initCfg() {
27 | Cfg.LogLevel = "Debug"
28 | Cfg.InsecureSkipVerify = true
29 | Cfg.CiscoCompat = true
30 | Cfg.AgentName = ""
31 | Cfg.AgentVersion = "4.10.07062"
32 | }
33 |
--------------------------------------------------------------------------------
/base/log.go:
--------------------------------------------------------------------------------
1 | package base
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "path"
8 | "strings"
9 | )
10 |
11 | // 相当于枚举,只有小于设置级别的日志才会输出,不区分大小写
12 | // Debug < Info < Warn < Error < Fatal
13 | const (
14 | _Debug = iota
15 | _Info
16 | _Warn
17 | _Error
18 | _Fatal
19 | )
20 |
21 | var (
22 | baseWriter *logWriter
23 | baseLogger *log.Logger
24 | baseLevel int
25 | levels map[int]string
26 |
27 | logName = "vpnagent.log"
28 | )
29 |
30 | type logWriter struct {
31 | UseStdout bool
32 | FileName string
33 | File *os.File
34 | NowDate string
35 | }
36 |
37 | // 由 initLog() 中的 log.New 注册调用
38 | func (lw *logWriter) Write(p []byte) (n int, err error) {
39 | return lw.File.Write(p)
40 | }
41 |
42 | // 创建新文件
43 | func (lw *logWriter) newFile() {
44 | if lw.UseStdout {
45 | lw.File = os.Stdout
46 | return
47 | }
48 | if Cfg.LogPath != "" {
49 | err := os.MkdirAll(Cfg.LogPath, os.ModePerm)
50 | if err != nil {
51 | lw.File = os.Stdout
52 | Error(err)
53 | return
54 | }
55 | }
56 | // 客户端不需要内容追加,每次重启客户端或者配置更改重新生成干净日志,即使 root 权限,os.OpenFile 也不能打开其它用户文件,但能删除!
57 | _ = os.Remove(lw.FileName)
58 | f, err := os.OpenFile(lw.FileName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
59 | if err != nil {
60 | lw.File = os.Stdout
61 | Error(err)
62 | return
63 | }
64 | lw.File = f
65 | }
66 |
67 | func InitLog() {
68 | // 初始化 baseLogger
69 | baseWriter = &logWriter{
70 | UseStdout: Cfg.LogPath == "",
71 | FileName: path.Join(Cfg.LogPath, logName),
72 | }
73 | baseWriter.newFile()
74 | baseLevel = logLevel2Int(Cfg.LogLevel)
75 | baseLogger = log.New(baseWriter, "", log.LstdFlags|log.Lshortfile)
76 | }
77 |
78 | func GetBaseLogger() *log.Logger {
79 | return baseLogger
80 | }
81 |
82 | func logLevel2Int(l string) int {
83 | levels = map[int]string{
84 | _Debug: "Debug",
85 | _Info: "Info",
86 | _Warn: "Warn",
87 | _Error: "Error",
88 | _Fatal: "Fatal",
89 | }
90 | lvl := _Info
91 | for k, v := range levels {
92 | if strings.EqualFold(strings.ToLower(l), strings.ToLower(v)) {
93 | lvl = k
94 | }
95 | }
96 | return lvl
97 | }
98 |
99 | func output(l int, s ...interface{}) {
100 | lvl := fmt.Sprintf("[%s] ", levels[l])
101 | _ = baseLogger.Output(3, lvl+fmt.Sprintln(s...))
102 | }
103 |
104 | func Debug(v ...interface{}) {
105 | l := _Debug
106 | if baseLevel > l {
107 | return
108 | }
109 | output(l, v...)
110 | }
111 |
112 | func Info(v ...interface{}) {
113 | l := _Info
114 | if baseLevel > l {
115 | return
116 | }
117 | output(l, v...)
118 | }
119 |
120 | func Warn(v ...interface{}) {
121 | l := _Warn
122 | if baseLevel > l {
123 | return
124 | }
125 | output(l, v...)
126 | }
127 |
128 | func Error(v ...interface{}) {
129 | l := _Error
130 | if baseLevel > l {
131 | return
132 | }
133 | output(l, v...)
134 | }
135 |
136 | func Fatal(v ...interface{}) {
137 | l := _Fatal
138 | if baseLevel > l {
139 | return
140 | }
141 | output(l, v...)
142 | os.Exit(1)
143 | }
144 |
--------------------------------------------------------------------------------
/base/setup.go:
--------------------------------------------------------------------------------
1 | package base
2 |
3 | func Setup() {
4 | initCfg()
5 | // 默认启动日志作用于 rpc 服务启动和 UI 连接到 rpc 服务,UI 需要在连接成功或修改配置后主动推送配置
6 | InitLog()
7 | }
8 |
--------------------------------------------------------------------------------
/cmd/connect.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "strings"
7 |
8 | "github.com/apieasy/gson"
9 | "github.com/spf13/cobra"
10 | "golang.org/x/crypto/ssh/terminal"
11 | "sslcon/rpc"
12 | )
13 |
14 | var (
15 | host string
16 | username string
17 | password string
18 | group string
19 | secret string
20 |
21 | logLevel string
22 | logPath string
23 | )
24 |
25 | var connect = &cobra.Command{
26 | Use: "connect",
27 | Short: "Connect to the VPN server",
28 | // Args: cobra.MinimumNArgs(1), // 至少1个非选项参数
29 | Run: func(cmd *cobra.Command, args []string) {
30 | if host == "" || username == "" {
31 | cmd.Help()
32 | } else {
33 | if password == "" {
34 | fmt.Print("Enter your password:")
35 | bytePassword, err := terminal.ReadPassword(int(os.Stdin.Fd()))
36 | if err != nil {
37 | fmt.Println("Error reading password:", err)
38 | return
39 | }
40 | password = string(bytePassword)
41 | fmt.Println()
42 | }
43 | // fmt.Println(host, username, password, group)
44 | if password != "" {
45 | params := make(map[string]string)
46 | params["log_level"] = logLevel
47 | params["log_path"] = logPath
48 |
49 | result := gson.New()
50 | err := rpcCall("config", params, result, rpc.CONFIG)
51 | if err != nil {
52 | after, _ := strings.CutPrefix(err.Error(), "jsonrpc2: code 1 message: ")
53 | fmt.Println(after)
54 | } else {
55 | params := make(map[string]string)
56 | params["host"] = host
57 | params["username"] = username
58 | params["password"] = password
59 | params["group"] = group
60 | params["secret"] = secret
61 |
62 | err := rpcCall("connect", params, result, rpc.CONNECT)
63 | if err != nil {
64 | after, _ := strings.CutPrefix(err.Error(), "jsonrpc2: code 1 message: ")
65 | fmt.Println(after)
66 | } else {
67 | result.Print()
68 | }
69 | }
70 | }
71 | }
72 | },
73 | }
74 |
75 | func init() {
76 | // 子命令自己被编译、添加到主命令当中
77 | rootCmd.AddCommand(connect)
78 |
79 | // 将 Flag 解析到全局变量
80 | connect.Flags().StringVarP(&host, "server", "s", "", "VPN server")
81 | connect.Flags().StringVarP(&username, "username", "u", "", "User name")
82 | connect.Flags().StringVarP(&password, "password", "p", "", "User password")
83 | connect.Flags().StringVarP(&group, "group", "g", "", "User group")
84 | connect.Flags().StringVarP(&secret, "key", "k", "", "Secret key")
85 |
86 | connect.Flags().StringVarP(&logLevel, "log_level", "l", "info", "Set the log level")
87 | connect.Flags().StringVarP(&logPath, "log_path", "d", os.TempDir(), "Set the log directory")
88 | }
89 |
--------------------------------------------------------------------------------
/cmd/disconnect.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/apieasy/gson"
8 | "github.com/spf13/cobra"
9 | "sslcon/rpc"
10 | )
11 |
12 | var disconnect = &cobra.Command{
13 | Use: "disconnect",
14 | Short: "Disconnect from the VPN server",
15 | Run: func(cmd *cobra.Command, args []string) {
16 | result := gson.New()
17 | err := rpcCall("disconnect", nil, result, rpc.DISCONNECT)
18 | if err != nil {
19 | after, _ := strings.CutPrefix(err.Error(), "jsonrpc2: code 1 message: ")
20 | fmt.Println(after)
21 | } else {
22 | result.Print()
23 | }
24 | },
25 | }
26 |
27 | func init() {
28 | rootCmd.AddCommand(disconnect)
29 | }
30 |
--------------------------------------------------------------------------------
/cmd/jsonrpc.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/gorilla/websocket"
7 | "github.com/sourcegraph/jsonrpc2"
8 | ws "github.com/sourcegraph/jsonrpc2/websocket"
9 | )
10 |
11 | // type handler struct{}
12 | //
13 | // func (_ *handler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) {}
14 | //
15 | // var rpcHandler = handler{}
16 |
17 | func rpcCall(method string, params interface{}, result interface{}, id uint64) error {
18 | conn, _, err := websocket.DefaultDialer.Dial("ws://127.0.0.1:6210/rpc", nil)
19 | if err != nil {
20 | return err
21 | }
22 | jsonStream := ws.NewObjectStream(conn)
23 | ctx := context.Background()
24 | rpcConn := jsonrpc2.NewConn(ctx, jsonStream, nil)
25 | defer rpcConn.Close()
26 |
27 | return rpcConn.Call(ctx, method, params, result, jsonrpc2.PickID(jsonrpc2.ID{Num: id}))
28 | }
29 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | var rootCmd = &cobra.Command{
11 | Use: "sslcon",
12 | Long: `A CLI application that supports the OpenConnect SSL VPN protocol.
13 | For more information, please visit https://github.com/tlslink/sslcon`,
14 | CompletionOptions: cobra.CompletionOptions{HiddenDefaultCmd: true},
15 | // rootCmd.Execute() 执行完成之前调用
16 | Run: func(cmd *cobra.Command, args []string) { // 若执行子命令或者帮助或者出现错误,则不会执行这里
17 | cmd.Help()
18 | },
19 | }
20 |
21 | func Execute() {
22 | rootCmd.SetHelpCommand(&cobra.Command{Hidden: true})
23 | if err := rootCmd.Execute(); err != nil {
24 | fmt.Println(err)
25 | os.Exit(1)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/cmd/status.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/apieasy/gson"
8 | "github.com/spf13/cobra"
9 | "sslcon/rpc"
10 | )
11 |
12 | var status = &cobra.Command{
13 | Use: "status",
14 | Short: "Get VPN connection information",
15 | Run: func(cmd *cobra.Command, args []string) {
16 | result := gson.New()
17 | err := rpcCall("status", nil, result, rpc.STATUS)
18 | if err != nil {
19 | after, _ := strings.CutPrefix(err.Error(), "jsonrpc2: code 1 message: ")
20 | fmt.Println(after)
21 | } else {
22 | result.Print()
23 | }
24 | },
25 | }
26 |
27 | func init() {
28 | rootCmd.AddCommand(status)
29 | }
30 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module sslcon
2 |
3 | go 1.23.0
4 |
5 | require (
6 | github.com/apieasy/gson v0.2.2
7 | github.com/elastic/go-sysinfo v1.15.0
8 | github.com/gopacket/gopacket v1.3.0
9 | github.com/gorilla/websocket v1.5.3
10 | github.com/jackpal/gateway v1.0.15
11 | github.com/kardianos/service v1.2.2
12 | github.com/lysShub/wintun-go v0.0.0-20240606130541-1acbbbe408f3
13 | github.com/pion/dtls/v3 v3.0.4
14 | github.com/sourcegraph/jsonrpc2 v0.2.0
15 | github.com/spf13/cobra v1.8.1
16 | github.com/vishvananda/netlink v1.3.0
17 | go.uber.org/atomic v1.11.0
18 | golang.org/x/crypto v0.29.0
19 | golang.org/x/net v0.31.0
20 | golang.org/x/sys v0.27.0
21 | golang.zx2c4.com/wireguard/windows v0.5.3
22 | )
23 |
24 | require (
25 | github.com/davecgh/go-spew v1.1.1 // indirect
26 | github.com/elastic/go-windows v1.0.2 // indirect
27 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
28 | github.com/lysShub/divert-go v0.0.0-20240811192723-79d7e0ef450e // indirect
29 | github.com/pion/logging v0.2.2 // indirect
30 | github.com/pion/transport/v3 v3.0.7 // indirect
31 | github.com/pkg/errors v0.9.1 // indirect
32 | github.com/pmezard/go-difflib v1.0.0 // indirect
33 | github.com/prometheus/procfs v0.15.1 // indirect
34 | github.com/spf13/pflag v1.0.5 // indirect
35 | github.com/stretchr/objx v0.5.2 // indirect
36 | github.com/stretchr/testify v1.9.0 // indirect
37 | github.com/tidwall/gjson v1.18.0 // indirect
38 | github.com/tidwall/match v1.1.1 // indirect
39 | github.com/tidwall/pretty v1.2.1 // indirect
40 | github.com/tidwall/sjson v1.2.5 // indirect
41 | github.com/vishvananda/netns v0.0.5 // indirect
42 | golang.org/x/term v0.26.0 // indirect
43 | gopkg.in/yaml.v3 v3.0.1 // indirect
44 | howett.net/plist v1.0.1 // indirect
45 | )
46 |
47 | replace github.com/kardianos/service v1.2.2 => github.com/cuonglm/service v0.0.0-20230322120818-ee0647d95905
48 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/apieasy/gson v0.2.2 h1:yQtZoT4QuDaYL+08ZAge4cl/XyvIaoz64AAt13vtsCU=
2 | github.com/apieasy/gson v0.2.2/go.mod h1:7G5ovd0ChVrS75yPwwReVmvK5M8NAw/GDVkxsR13xbQ=
3 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
4 | github.com/cuonglm/service v0.0.0-20230322120818-ee0647d95905 h1:pDgansvDPj1+gpA9aZwTbPzl0py9bmb3IYBhUD7a8tQ=
5 | github.com/cuonglm/service v0.0.0-20230322120818-ee0647d95905/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8 | github.com/elastic/go-sysinfo v1.15.0 h1:54pRFlAYUlVNQ2HbXzLVZlV+fxS7Eax49stzg95M4Xw=
9 | github.com/elastic/go-sysinfo v1.15.0/go.mod h1:jPSuTgXG+dhhh0GKIyI2Cso+w5lPJ5PvVqKlL8LV/Hk=
10 | github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI=
11 | github.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLixie9u9ixLM8=
12 | github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
13 | github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
14 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
15 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
16 | github.com/gopacket/gopacket v1.3.0 h1:MouZCc+ej0vnqzB0WeiaO/6+tGvb+KU7UczxoQ+X0Yc=
17 | github.com/gopacket/gopacket v1.3.0/go.mod h1:WnFrU1Xkf5lWKV38uKNR9+yYtppn+ZYzOyNqMeH4oNE=
18 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
19 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
20 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
21 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
22 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
23 | github.com/jackpal/gateway v1.0.15 h1:yb4Gltgr8ApHWWnSyybnDL1vURbqw7ooo7IIL5VZSeg=
24 | github.com/jackpal/gateway v1.0.15/go.mod h1:dbyEDcDhHUh9EmjB9ung81elMUZfG0SoNc2TfTbcj4c=
25 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
26 | github.com/lysShub/divert-go v0.0.0-20240811192723-79d7e0ef450e h1:BDCvQkFWGrd6mNqS331RIwrTJmBOcDO9kdM60iSB6HE=
27 | github.com/lysShub/divert-go v0.0.0-20240811192723-79d7e0ef450e/go.mod h1:OXuD4Q/Y84FyNiYy/sf9RVshvAC5/rvcHA6J7JvvtFM=
28 | github.com/lysShub/wintun-go v0.0.0-20240606130541-1acbbbe408f3 h1:/XulQgCtosa4ZM6Od0r2i1MHNj1zh8aBnzan/L5nlzU=
29 | github.com/lysShub/wintun-go v0.0.0-20240606130541-1acbbbe408f3/go.mod h1:+Xxb+IInu6P1p9wKo6JYEG8OEYdvEKxgpwFSGJ4vqS0=
30 | github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U=
31 | github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg=
32 | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
33 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
34 | github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
35 | github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
36 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
37 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
38 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
39 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
40 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
41 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
42 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
43 | github.com/sourcegraph/jsonrpc2 v0.2.0 h1:KjN/dC4fP6aN9030MZCJs9WQbTOjWHhrtKVpzzSrr/U=
44 | github.com/sourcegraph/jsonrpc2 v0.2.0/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo=
45 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
46 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
47 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
48 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
49 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
50 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
51 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
52 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
53 | github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
54 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
55 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
56 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
57 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
58 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
59 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
60 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
61 | github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
62 | github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
63 | github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
64 | github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
65 | github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
66 | github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
67 | github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
68 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
69 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
70 | golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
71 | golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
72 | golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
73 | golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
74 | golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
75 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
76 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
77 | golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
78 | golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
79 | golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
80 | golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
81 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
82 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
83 | golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
84 | golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
85 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
86 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
87 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
88 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
89 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
90 | gvisor.dev/gvisor v0.0.0-20230916030846-1d82564559db h1:CO57Wj9fblWZhyk6rViybNDtdHr9AgiuAzVzD4aFMjE=
91 | gvisor.dev/gvisor v0.0.0-20230916030846-1d82564559db/go.mod h1:lYEMhXbxgudVhALYsMQrBaUAjM3NMinh8mKL1CJv7rc=
92 | howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
93 | howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
94 |
--------------------------------------------------------------------------------
/proto/dtd.go:
--------------------------------------------------------------------------------
1 | package proto
2 |
3 | import "encoding/xml"
4 |
5 | // DTD 基于 XML 的客户端、服务端请求和响应数据结构
6 | // https://datatracker.ietf.org/doc/html/draft-mavrogiannopoulos-openconnect-03#appendix-C.1
7 | type DTD struct {
8 | XMLName xml.Name `xml:"config-auth"`
9 | Client string `xml:"client,attr"` // 一般都是 vpn
10 | Type string `xml:"type,attr"` // 请求类型 init logout auth-reply
11 | AggregateAuthVersion string `xml:"aggregate-auth-version,attr"` // 一般都是 2
12 | Version string `xml:"version"` // 客户端版本号
13 | GroupAccess string `xml:"group-access"` // 请求的地址
14 | GroupSelect string `xml:"group-select"` // 选择的组名
15 | SessionToken string `xml:"session-token"`
16 | Auth auth `xml:"auth"`
17 | DeviceId deviceId `xml:"device-id"`
18 | Opaque opaque `xml:"opaque"`
19 | MacAddressList macAddressList `xml:"mac-address-list"`
20 | Config config `xml:"config"`
21 | }
22 |
23 | type auth struct {
24 | Username string `xml:"username"`
25 | Password string `xml:"password"`
26 | Message string `xml:"message"`
27 | Banner string `xml:"banner"`
28 | Error authError `xml:"error"`
29 | Form form `xml:"form"`
30 | }
31 |
32 | type form struct {
33 | Action string `xml:"action,attr"`
34 | Groups []string `xml:"select>option"`
35 | }
36 |
37 | type authError struct {
38 | Param1 string `xml:"param1,attr"`
39 | Value string `xml:",chardata"`
40 | }
41 |
42 | type deviceId struct {
43 | ComputerName string `xml:"computer-name,attr"`
44 | DeviceType string `xml:"device-type,attr"`
45 | PlatformVersion string `xml:"platform-version,attr"`
46 | UniqueId string `xml:"unique-id,attr"`
47 | UniqueIdGlobal string `xml:"unique-id-global,attr"`
48 | }
49 |
50 | type opaque struct {
51 | TunnelGroup string `xml:"tunnel-group"`
52 | GroupAlias string `xml:"group-alias"`
53 | ConfigHash string `xml:"config-hash"`
54 | }
55 |
56 | type macAddressList struct {
57 | MacAddress string `xml:"mac-address"`
58 | }
59 |
60 | type config struct {
61 | Opaque opaque2 `xml:"opaque"`
62 | }
63 |
64 | type opaque2 struct {
65 | CustomAttr customAttr `xml:"custom-attr"`
66 | }
67 |
68 | type customAttr struct {
69 | DynamicSplitExcludeDomains string `xml:"dynamic-split-exclude-domains"`
70 | DynamicSplitIncludeDomains string `xml:"dynamic-split-include-domains"`
71 | }
72 |
--------------------------------------------------------------------------------
/proto/protocol.go:
--------------------------------------------------------------------------------
1 | package proto
2 |
3 | // packet = header + Data,The whole packet in encapsulated in a TLS record (see [RFC8446])
4 | // 只用于 tls 数据包,dtls 数据包只有第 6 个字节
5 |
6 | var Header = []byte{
7 | 0x53, 0x54, 0x46, 0x01, // fixed to 0x53 (S) 0x54 (T) 0x46 (F) 0x01
8 | 0x00, 0x00, // The length of the packet that follows this header in big endian order
9 | 0x00, // The type of the payload that follows
10 | 0x00, // fixed to 0x00
11 | }
12 |
13 | // Payload 缓冲区数据结构
14 | type Payload struct {
15 | Type byte // The available payload types
16 | Data []byte
17 | }
18 |
19 | // https://datatracker.ietf.org/doc/html/draft-mavrogiannopoulos-openconnect-04#name-the-cstp-channel-protocol
20 | /*
21 | var header = []byte{'S', 'T', 'F', 0x01, 0, 0, 0x00, 0}
22 | +---------------------+---------------------------------------------+
23 | | byte | value |
24 | +---------------------+---------------------------------------------+
25 | | 0 | fixed to 0x53 (S) |
26 | | | |
27 | | 1 | fixed to 0x54 (T) |
28 | | | |
29 | | 2 | fixed to 0x46 (F) |
30 | | | |
31 | | 3 | fixed to 0x01 |
32 | | | |
33 | | 4-5 | The length of the packet that follows this |
34 | | | header in big endian order |
35 | | | |
36 | | 6 | The type of the payload that follows (see |
37 | | | Table 3 for available types) |
38 | | | |
39 | | 7 | fixed to 0x00 |
40 | +---------------------+---------------------------------------------+
41 |
42 |
43 | The available payload types.
44 | +---------------------+---------------------------------------------+
45 | | Value | Description |
46 | +---------------------+---------------------------------------------+
47 | | 0x00 | DATA: the TLS record packet contains an |
48 | | | IPv4 or IPv6 packet |
49 | | | |
50 | | 0x03 | DPD-REQ: used for dead peer detection. Once |
51 | | | sent the peer should reply with a DPD-RESP |
52 | | | packet, that has the same contents as the |
53 | | | original request. |
54 | | | |
55 | | 0x04 | DPD-RESP: used as a response to a |
56 | | | previously received DPD-REQ. |
57 | | | |
58 | | 0x05 | DISCONNECT: sent by the client (or server) |
59 | | | to terminate the session. No data is |
60 | | | associated with this request. The session |
61 | | | will be invalidated after such request. |
62 | | | |
63 | | 0x07 | KEEPALIVE: sent by any peer. No data is |
64 | | | associated with this request. |
65 | | | |
66 | | 0x08 | COMPRESSED DATA: a Data packet which is |
67 | | | compressed prior to encryption. |
68 | | | |
69 | | 0x09 | TERMINATE: sent by the server to indicate |
70 | | | that the server is shutting down. No data |
71 | | | is associated with this request. |
72 | +---------------------+---------------------------------------------+
73 | */
74 |
75 | // Security https://datatracker.ietf.org/doc/html/draft-mavrogiannopoulos-openconnect-04#name-security-considerations
76 |
--------------------------------------------------------------------------------
/rpc/connect.go:
--------------------------------------------------------------------------------
1 | package rpc
2 |
3 | import (
4 | "strings"
5 |
6 | "sslcon/auth"
7 | "sslcon/session"
8 | "sslcon/utils/vpnc"
9 | "sslcon/vpn"
10 | )
11 |
12 | // Connect 调用之前必须由前端填充 auth.Prof,建议填充 base.Interface
13 | func Connect() error {
14 | if strings.Contains(auth.Prof.Host, ":") {
15 | auth.Prof.HostWithPort = auth.Prof.Host
16 | } else {
17 | auth.Prof.HostWithPort = auth.Prof.Host + ":443"
18 | }
19 | if !auth.Prof.Initialized {
20 | err := vpnc.GetLocalInterface()
21 | if err != nil {
22 | return err
23 | }
24 | }
25 | err := auth.InitAuth()
26 | if err != nil {
27 | return err
28 | }
29 | err = auth.PasswordAuth()
30 | if err != nil {
31 | return err
32 | }
33 |
34 | return SetupTunnel(false)
35 | }
36 |
37 | // SetupTunnel 操作系统长时间睡眠后再自动连接会失败,仅用于短时间断线自动重连
38 | func SetupTunnel(reconnect bool) error {
39 | // 为适应复杂网络环境,必须能够感知网卡变化,建议由前端获取当前网络信息发送过来,而不是登陆前由 Go 处理
40 | // 断网重连时网卡信息可能已经变化,所以建立隧道时重新获取网卡信息
41 | if reconnect && !auth.Prof.Initialized {
42 | err := vpnc.GetLocalInterface()
43 | if err != nil {
44 | return err
45 | }
46 | }
47 | return vpn.SetupTunnel()
48 | }
49 |
50 | // DisConnect 主动断开或者 ctrl+c,不包括网络或tun异常退出
51 | func DisConnect() {
52 | session.Sess.ActiveClose = true
53 | if session.Sess.CSess != nil {
54 | vpnc.ResetRoutes(session.Sess.CSess) // 蛋疼的循环引用
55 | session.Sess.CSess.Close()
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/rpc/rpc.go:
--------------------------------------------------------------------------------
1 | package rpc
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "net/http"
8 | "runtime/debug"
9 |
10 | "github.com/gorilla/websocket"
11 | "github.com/sourcegraph/jsonrpc2"
12 | ws "github.com/sourcegraph/jsonrpc2/websocket"
13 | "sslcon/auth"
14 | "sslcon/base"
15 | "sslcon/session"
16 | )
17 |
18 | const (
19 | STATUS = iota
20 | CONFIG
21 | CONNECT
22 | DISCONNECT
23 | RECONNECT
24 | INTERFACE
25 | ABORT
26 | STAT
27 | )
28 |
29 | var (
30 | Clients []*jsonrpc2.Conn
31 | rpcHandler = handler{}
32 | connectedStr string
33 | disconnectedStr string
34 | )
35 |
36 | type handler struct{}
37 |
38 | func Setup() {
39 | go func() {
40 | http.HandleFunc("/rpc", rpc)
41 | // 无法启动则退出服务或应用,监听本地不需要有效物理网卡
42 | base.Fatal(http.ListenAndServe(":6210", nil))
43 | }()
44 | }
45 |
46 | func rpc(resp http.ResponseWriter, req *http.Request) {
47 | up := websocket.Upgrader{
48 | CheckOrigin: func(r *http.Request) bool {
49 | return true
50 | },
51 | }
52 | conn, err := up.Upgrade(resp, req, nil)
53 | if err != nil {
54 | base.Error(err)
55 | return
56 | }
57 | defer conn.Close()
58 |
59 | jsonStream := ws.NewObjectStream(conn)
60 | // 此时 base.GetBaseLogger() 仍然是 Stdout,当前使用的 rpc 库无法在连接成功后修改 logger
61 | rpcConn := jsonrpc2.NewConn(req.Context(), jsonStream, &rpcHandler, jsonrpc2.SetLogger(base.GetBaseLogger()))
62 | Clients = append(Clients, rpcConn)
63 | <-rpcConn.DisconnectNotify()
64 | for i, c := range Clients {
65 | if c == rpcConn {
66 | Clients = append(Clients[:i], Clients[i+1:]...)
67 | base.Debug(fmt.Sprintf("client %d disconnected", i))
68 | break
69 | }
70 | }
71 | }
72 |
73 | // Handle ID 即方法
74 | func (_ *handler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) {
75 | defer func() {
76 | if err := recover(); err != nil {
77 | base.Error(string(debug.Stack()))
78 | }
79 | }()
80 |
81 | // request route
82 | switch req.ID.Num {
83 | case STAT:
84 | // 未连接之前不应该调用这里
85 | if session.Sess.CSess != nil {
86 | _ = conn.Reply(ctx, req.ID, session.Sess.CSess.Stat)
87 | return
88 | }
89 | jError := jsonrpc2.Error{Code: 1, Message: disconnectedStr}
90 | _ = conn.ReplyWithError(ctx, req.ID, &jError)
91 | case STATUS:
92 | // 未连接之前不应该调用这里
93 | if session.Sess.CSess != nil {
94 | if !base.Cfg.NoDTLS && session.Sess.CSess.DTLSPort != "" {
95 | // 等待 DTLS 隧道创建过程结束,无论隧道是否建立成功
96 | <-session.Sess.CSess.DtlsSetupChan
97 | }
98 |
99 | if session.Sess.CSess != nil {
100 | _ = conn.Reply(ctx, req.ID, session.Sess.CSess)
101 | return
102 | }
103 | }
104 |
105 | jError := jsonrpc2.Error{Code: 1, Message: disconnectedStr}
106 | _ = conn.ReplyWithError(ctx, req.ID, &jError)
107 | case CONNECT:
108 | // 启动时未连接,其它 UI 连接后再次调用
109 | if session.Sess.CSess != nil {
110 | _ = conn.Reply(ctx, req.ID, connectedStr)
111 | return
112 | }
113 | err := json.Unmarshal(*req.Params, auth.Prof)
114 | if err != nil {
115 | jError := jsonrpc2.Error{Code: 1, Message: err.Error()}
116 | _ = conn.ReplyWithError(ctx, req.ID, &jError)
117 | return
118 | }
119 | err = Connect()
120 | if err != nil {
121 | base.Error(err)
122 | jError := jsonrpc2.Error{Code: 1, Message: err.Error()}
123 | _ = conn.ReplyWithError(ctx, req.ID, &jError)
124 | DisConnect()
125 | return
126 | }
127 | connectedStr = "connected to " + auth.Prof.Host
128 | disconnectedStr = "disconnected from " + auth.Prof.Host
129 | _ = conn.Reply(ctx, req.ID, connectedStr)
130 | go monitor()
131 | case RECONNECT:
132 | // UI 未检测到活动网络发生变化或者网络变化后已经推送接口信息
133 | if session.Sess.CSess != nil {
134 | _ = conn.Reply(ctx, req.ID, connectedStr)
135 | return
136 | }
137 | err := SetupTunnel(true)
138 | if err != nil {
139 | base.Error(err)
140 | jError := jsonrpc2.Error{Code: 1, Message: err.Error()}
141 | _ = conn.ReplyWithError(ctx, req.ID, &jError)
142 | DisConnect()
143 | return
144 | }
145 | _ = conn.Reply(ctx, req.ID, connectedStr)
146 | go monitor()
147 | case DISCONNECT:
148 | if session.Sess.CSess != nil {
149 | DisConnect()
150 | } else {
151 | jError := jsonrpc2.Error{Code: 1, Message: disconnectedStr}
152 | _ = conn.ReplyWithError(ctx, req.ID, &jError)
153 | }
154 | case CONFIG:
155 | // 初始化配置
156 | err := json.Unmarshal(*req.Params, &base.Cfg)
157 | if err != nil {
158 | jError := jsonrpc2.Error{Code: 1, Message: err.Error()}
159 | _ = conn.ReplyWithError(ctx, req.ID, &jError)
160 | return
161 | }
162 | _ = conn.Reply(ctx, req.ID, "ready to connect")
163 | // 每次重启客户端或者配置更改,重置 logger
164 | base.InitLog()
165 | case INTERFACE:
166 | err := json.Unmarshal(*req.Params, base.LocalInterface)
167 | if err != nil {
168 | jError := jsonrpc2.Error{Code: 1, Message: err.Error()}
169 | _ = conn.ReplyWithError(ctx, req.ID, &jError)
170 | return
171 | }
172 | auth.Prof.Initialized = true
173 | _ = conn.Reply(ctx, req.ID, "ready to connect")
174 | default:
175 | base.Debug("receive rpc call:", req)
176 | jError := jsonrpc2.Error{Code: 1, Message: "unknown method: " + req.Method}
177 | _ = conn.ReplyWithError(ctx, req.ID, &jError)
178 | }
179 | }
180 |
181 | func monitor() {
182 | // 不考虑 DTLS 中途关闭情形
183 | <-session.Sess.CloseChan
184 | ctx := context.Background()
185 | for _, conn := range Clients {
186 | if session.Sess.ActiveClose {
187 | _ = conn.Reply(ctx, jsonrpc2.ID{Num: DISCONNECT, IsString: false}, disconnectedStr)
188 | } else {
189 | _ = conn.Reply(ctx, jsonrpc2.ID{Num: ABORT, IsString: false}, disconnectedStr)
190 | }
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/session/session.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | import (
4 | "encoding/xml"
5 | "net/http"
6 | "strconv"
7 | "strings"
8 | "sync"
9 | "time"
10 |
11 | "go.uber.org/atomic"
12 | "sslcon/base"
13 | "sslcon/proto"
14 | "sslcon/utils"
15 | )
16 |
17 | var (
18 | Sess = &Session{}
19 | )
20 |
21 | type Session struct {
22 | SessionToken string
23 | PreMasterSecret []byte
24 |
25 | ActiveClose bool
26 | CloseChan chan struct{} // 用于通知所有 UI,ConnSession 已关闭
27 | CSess *ConnSession
28 | }
29 |
30 | type stat struct {
31 | // be sure to use the double type when parsing
32 | BytesSent uint64 `json:"bytesSent"`
33 | BytesReceived uint64 `json:"bytesReceived"`
34 | }
35 |
36 | // ConnSession used for both TLS and DTLS
37 | type ConnSession struct {
38 | Sess *Session `json:"-"`
39 |
40 | ServerAddress string
41 | LocalAddress string
42 | Hostname string
43 | TunName string
44 | VPNAddress string // The IPv4 address of the client
45 | VPNMask string // IPv4 netmask
46 | DNS []string
47 | MTU int
48 | SplitInclude []string
49 | SplitExclude []string
50 |
51 | DynamicSplitTunneling bool
52 | DynamicSplitIncludeDomains []string
53 | DynamicSplitIncludeResolved sync.Map // https://github.com/golang/go/issues/31136
54 | DynamicSplitExcludeDomains []string
55 | DynamicSplitExcludeResolved sync.Map
56 |
57 | TLSCipherSuite string
58 | TLSDpdTime int // https://datatracker.ietf.org/doc/html/rfc3706
59 | TLSKeepaliveTime int
60 | DTLSPort string
61 | DTLSDpdTime int
62 | DTLSKeepaliveTime int
63 | DTLSId string `json:"-"` // used by the server to associate the DTLS channel with the CSTP channel
64 | DTLSCipherSuite string
65 | Stat *stat
66 |
67 | closeOnce sync.Once `json:"-"`
68 | CloseChan chan struct{} `json:"-"`
69 | PayloadIn chan *proto.Payload `json:"-"`
70 | PayloadOutTLS chan *proto.Payload `json:"-"`
71 | PayloadOutDTLS chan *proto.Payload `json:"-"`
72 |
73 | DtlsConnected *atomic.Bool
74 | DtlsSetupChan chan struct{} `json:"-"`
75 | DSess *DtlsSession `json:"-"`
76 |
77 | ResetTLSReadDead *atomic.Bool `json:"-"`
78 | ResetDTLSReadDead *atomic.Bool `json:"-"`
79 | }
80 |
81 | type DtlsSession struct {
82 | closeOnce sync.Once
83 | CloseChan chan struct{}
84 | }
85 |
86 | func (sess *Session) NewConnSession(header *http.Header) *ConnSession {
87 | cSess := &ConnSession{
88 | Sess: sess,
89 | LocalAddress: base.LocalInterface.Ip4,
90 | Stat: &stat{0, 0},
91 | closeOnce: sync.Once{},
92 | CloseChan: make(chan struct{}),
93 | DtlsSetupChan: make(chan struct{}),
94 | PayloadIn: make(chan *proto.Payload, 64),
95 | PayloadOutTLS: make(chan *proto.Payload, 64),
96 | PayloadOutDTLS: make(chan *proto.Payload, 64),
97 | DtlsConnected: atomic.NewBool(false),
98 | ResetTLSReadDead: atomic.NewBool(true),
99 | ResetDTLSReadDead: atomic.NewBool(true),
100 | DSess: &DtlsSession{
101 | closeOnce: sync.Once{},
102 | CloseChan: make(chan struct{}),
103 | },
104 | }
105 | sess.CSess = cSess
106 |
107 | sess.ActiveClose = false
108 | sess.CloseChan = make(chan struct{})
109 |
110 | cSess.VPNAddress = header.Get("X-CSTP-Address")
111 | cSess.VPNMask = header.Get("X-CSTP-Netmask")
112 | cSess.MTU, _ = strconv.Atoi(header.Get("X-CSTP-MTU"))
113 | cSess.DNS = header.Values("X-CSTP-DNS")
114 | // 如果服务器下发空字符串,字符串数组不会为 nil,会导致解析ip时报错
115 | cSess.SplitInclude = header.Values("X-CSTP-Split-Include")
116 | cSess.SplitExclude = header.Values("X-CSTP-Split-Exclude")
117 | // debug with https://ip.900cha.com/
118 | // cSess.SplitExclude = append(cSess.SplitExclude, "47.243.165.103/255.255.255.255")
119 |
120 | cSess.TLSDpdTime, _ = strconv.Atoi(header.Get("X-CSTP-DPD"))
121 | cSess.TLSKeepaliveTime, _ = strconv.Atoi(header.Get("X-CSTP-Keepalive"))
122 | // https://datatracker.ietf.org/doc/html/draft-mavrogiannopoulos-openconnect-02#section-2.1.5.1
123 | cSess.DTLSId = header.Get("X-DTLS-Session-ID")
124 | if cSess.DTLSId == "" {
125 | // 兼容最新 ocserv
126 | cSess.DTLSId = header.Get("X-DTLS-App-ID")
127 | }
128 | cSess.DTLSPort = header.Get("X-DTLS-Port")
129 | cSess.DTLSDpdTime, _ = strconv.Atoi(header.Get("X-DTLS-DPD"))
130 | cSess.DTLSKeepaliveTime, _ = strconv.Atoi(header.Get("X-DTLS-Keepalive"))
131 | if base.Cfg.NoDTLS {
132 | cSess.DTLSCipherSuite = "Unknown"
133 | } else {
134 | cSess.DTLSCipherSuite = header.Get("X-DTLS12-CipherSuite") // 连接前后格式不同
135 | }
136 |
137 | postAuth := header.Get("X-CSTP-Post-Auth-XML")
138 | if postAuth != "" {
139 | dtd := proto.DTD{}
140 | err := xml.Unmarshal([]byte(postAuth), &dtd)
141 | if err == nil {
142 | if dtd.Config.Opaque.CustomAttr.DynamicSplitIncludeDomains != "" {
143 | cSess.DynamicSplitIncludeDomains = strings.Split(dtd.Config.Opaque.CustomAttr.DynamicSplitIncludeDomains, ",")
144 | cSess.DynamicSplitTunneling = true
145 | } else if dtd.Config.Opaque.CustomAttr.DynamicSplitExcludeDomains != "" {
146 | // 字符串最后多一个逗号,导致数组最后一个元素为 "",不排除配置错误其它元素也为空的可能,go 没有直接删除容器元素的方法,这里不处理
147 | cSess.DynamicSplitExcludeDomains = strings.Split(dtd.Config.Opaque.CustomAttr.DynamicSplitExcludeDomains, ",")
148 | cSess.DynamicSplitTunneling = true
149 | }
150 |
151 | }
152 | }
153 |
154 | return cSess
155 | }
156 |
157 | func (cSess *ConnSession) DPDTimer() {
158 | go func() {
159 | defer func() {
160 | base.Info("dead peer detection timer exit")
161 | }()
162 | base.Debug("TLSDpdTime:", cSess.TLSDpdTime, "TLSKeepaliveTime", cSess.TLSKeepaliveTime,
163 | "DTLSDpdTime", cSess.DTLSDpdTime, "DTLSKeepaliveTime", cSess.DTLSKeepaliveTime)
164 | // 简化处理,最小15秒检测一次,至少5秒冗余
165 | dpdTime := utils.Min(cSess.TLSDpdTime, cSess.DTLSDpdTime) - 5
166 | if dpdTime < 10 {
167 | dpdTime = 10
168 | }
169 | ticker := time.NewTicker(time.Duration(dpdTime) * time.Second)
170 |
171 | tlsDpd := proto.Payload{
172 | Type: 0x03,
173 | Data: make([]byte, 0, 8),
174 | }
175 | dtlsDpd := proto.Payload{
176 | Type: 0x03,
177 | Data: make([]byte, 0, 1),
178 | }
179 |
180 | for {
181 | select {
182 | case <-ticker.C:
183 | // base.Debug("dead peer detection")
184 | select {
185 | case cSess.PayloadOutTLS <- &tlsDpd:
186 | default:
187 | }
188 | if cSess.DtlsConnected.Load() {
189 | select {
190 | case cSess.PayloadOutDTLS <- &dtlsDpd:
191 | default:
192 | }
193 | }
194 | case <-cSess.CloseChan:
195 | ticker.Stop()
196 | return
197 | }
198 | }
199 | }()
200 | }
201 |
202 | func (cSess *ConnSession) ReadDeadTimer() {
203 | go func() {
204 | defer func() {
205 | base.Info("read dead timer exit")
206 | }()
207 | // 避免每次 for 循环都重置读超时的时间
208 | // 这里是绝对时间,至于链接本身,服务器没有数据时 conn.Read 会阻塞,有数据时会不断判断
209 | ticker := time.NewTicker(4 * time.Second)
210 | for range ticker.C {
211 | select {
212 | case <-cSess.CloseChan:
213 | ticker.Stop()
214 | return
215 | default:
216 | cSess.ResetTLSReadDead.Store(true)
217 | cSess.ResetDTLSReadDead.Store(true)
218 | }
219 | }
220 | }()
221 | }
222 |
223 | func (cSess *ConnSession) Close() {
224 | cSess.closeOnce.Do(func() {
225 | if cSess.DtlsConnected.Load() {
226 | cSess.DSess.Close()
227 | }
228 | close(cSess.CloseChan)
229 | Sess.CSess = nil
230 |
231 | close(Sess.CloseChan)
232 | })
233 | }
234 |
235 | func (dSess *DtlsSession) Close() {
236 | dSess.closeOnce.Do(func() {
237 | close(dSess.CloseChan)
238 | if Sess.CSess != nil {
239 | Sess.CSess.DtlsConnected.Store(false)
240 | Sess.CSess.DTLSCipherSuite = ""
241 | }
242 | })
243 | }
244 |
--------------------------------------------------------------------------------
/sslcon.go:
--------------------------------------------------------------------------------
1 | //go:build linux || darwin || windows
2 |
3 | package main
4 |
5 | import "sslcon/cmd"
6 |
7 | func main() {
8 | cmd.Execute()
9 | }
10 |
--------------------------------------------------------------------------------
/svc/service.go:
--------------------------------------------------------------------------------
1 | package svc
2 |
3 | import (
4 | "fmt"
5 | "runtime"
6 |
7 | "github.com/kardianos/service"
8 | "sslcon/base"
9 | "sslcon/rpc"
10 | )
11 |
12 | type program struct{}
13 |
14 | var logger service.Logger
15 |
16 | var (
17 | serviceConfig *service.Config
18 | prg = &program{}
19 | )
20 |
21 | func init() {
22 | svcName := "sslcon"
23 | if runtime.GOOS == "windows" {
24 | svcName = "SSLCon"
25 | }
26 | serviceConfig = &service.Config{
27 | Name: svcName,
28 | DisplayName: "SSLCon VPN Agent",
29 | Description: "SSLCon SSL VPN service Agent",
30 | }
31 | }
32 |
33 | // Start should not block. Do the actual work async.
34 | func (p program) Start(s service.Service) error {
35 | if service.Interactive() {
36 | logger.Info("Running in terminal.")
37 | } else {
38 | logger.Info("Running under service manager.")
39 | }
40 | go p.run()
41 | return nil
42 | }
43 |
44 | // Stop should not block. Return with a few seconds.
45 | func (p program) Stop(s service.Service) error {
46 | logger.Info("I'm Stopping!")
47 | base.Info("Stop")
48 | rpc.DisConnect()
49 | return nil
50 | }
51 |
52 | func (p program) run() {
53 | base.Setup()
54 | rpc.Setup()
55 | }
56 |
57 | func RunSvc() {
58 | svc, err := service.New(prg, serviceConfig)
59 | if err != nil {
60 | fmt.Println("Cannot create the service: " + err.Error())
61 | }
62 | errs := make(chan error, 5)
63 | logger, err = svc.Logger(errs)
64 | if err != nil {
65 | fmt.Println("Cannot open a system logger: " + err.Error())
66 | }
67 | err = svc.Run()
68 | if err != nil {
69 | fmt.Println("Cannot start the service: " + err.Error())
70 | }
71 | }
72 |
73 | func InstallSvc() {
74 | svc, err := service.New(prg, serviceConfig)
75 | if err != nil {
76 | fmt.Println("Cannot create the service: " + err.Error())
77 | }
78 | err = svc.Install()
79 | if err != nil {
80 | fmt.Println("Cannot install the service: " + err.Error())
81 | } else {
82 | err := svc.Start()
83 | if err != nil {
84 | fmt.Println("Cannot start the service: " + err.Error())
85 | }
86 | }
87 | }
88 |
89 | func UninstallSvc() {
90 | svc, err := service.New(prg, serviceConfig)
91 | if err != nil {
92 | fmt.Println("Cannot create the service: " + err.Error())
93 | } else {
94 | err := svc.Stop()
95 | if err != nil {
96 | fmt.Println("Cannot stop the service: " + err.Error())
97 | }
98 | err = svc.Uninstall()
99 | if err != nil {
100 | fmt.Println("Cannot uninstall the service: " + err.Error())
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/tun/rwcancel/rwcancel.go:
--------------------------------------------------------------------------------
1 | //go:build !windows && !js
2 |
3 | /* SPDX-License-Identifier: MIT
4 | *
5 | * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
6 | */
7 |
8 | // Package rwcancel implements cancelable read/write operations on
9 | // a file descriptor.
10 | package rwcancel
11 |
12 | import (
13 | "errors"
14 | "os"
15 | "syscall"
16 |
17 | "golang.org/x/sys/unix"
18 | )
19 |
20 | type RWCancel struct {
21 | fd int
22 | closingReader *os.File
23 | closingWriter *os.File
24 | }
25 |
26 | func NewRWCancel(fd int) (*RWCancel, error) {
27 | err := unix.SetNonblock(fd, true)
28 | if err != nil {
29 | return nil, err
30 | }
31 | rwcancel := RWCancel{fd: fd}
32 |
33 | rwcancel.closingReader, rwcancel.closingWriter, err = os.Pipe()
34 | if err != nil {
35 | return nil, err
36 | }
37 |
38 | return &rwcancel, nil
39 | }
40 |
41 | func RetryAfterError(err error) bool {
42 | return errors.Is(err, syscall.EAGAIN) || errors.Is(err, syscall.EINTR)
43 | }
44 |
45 | func (rw *RWCancel) ReadyRead() bool {
46 | closeFd := int32(rw.closingReader.Fd())
47 |
48 | pollFds := []unix.PollFd{{Fd: int32(rw.fd), Events: unix.POLLIN}, {Fd: closeFd, Events: unix.POLLIN}}
49 | var err error
50 | for {
51 | _, err = unix.Poll(pollFds, -1)
52 | if err == nil || !RetryAfterError(err) {
53 | break
54 | }
55 | }
56 | if err != nil {
57 | return false
58 | }
59 | if pollFds[1].Revents != 0 {
60 | return false
61 | }
62 | return pollFds[0].Revents != 0
63 | }
64 |
65 | func (rw *RWCancel) ReadyWrite() bool {
66 | closeFd := int32(rw.closingReader.Fd())
67 | pollFds := []unix.PollFd{{Fd: int32(rw.fd), Events: unix.POLLOUT}, {Fd: closeFd, Events: unix.POLLOUT}}
68 | var err error
69 | for {
70 | _, err = unix.Poll(pollFds, -1)
71 | if err == nil || !RetryAfterError(err) {
72 | break
73 | }
74 | }
75 | if err != nil {
76 | return false
77 | }
78 |
79 | if pollFds[1].Revents != 0 {
80 | return false
81 | }
82 | return pollFds[0].Revents != 0
83 | }
84 |
85 | func (rw *RWCancel) Read(p []byte) (n int, err error) {
86 | for {
87 | n, err := unix.Read(rw.fd, p)
88 | if err == nil || !RetryAfterError(err) {
89 | return n, err
90 | }
91 | if !rw.ReadyRead() {
92 | return 0, os.ErrClosed
93 | }
94 | }
95 | }
96 |
97 | func (rw *RWCancel) Write(p []byte) (n int, err error) {
98 | for {
99 | n, err := unix.Write(rw.fd, p)
100 | if err == nil || !RetryAfterError(err) {
101 | return n, err
102 | }
103 | if !rw.ReadyWrite() {
104 | return 0, os.ErrClosed
105 | }
106 | }
107 | }
108 |
109 | func (rw *RWCancel) Cancel() (err error) {
110 | _, err = rw.closingWriter.Write([]byte{0})
111 | return
112 | }
113 |
114 | func (rw *RWCancel) Close() {
115 | rw.closingReader.Close()
116 | rw.closingWriter.Close()
117 | }
118 |
--------------------------------------------------------------------------------
/tun/tun.go:
--------------------------------------------------------------------------------
1 | // Package tun copy from https://git.zx2c4.com/wireguard-go/tree/tun/tun.go?h=0.0.20230223
2 | package tun
3 |
4 | import (
5 | "os"
6 | )
7 |
8 | var NativeTunDevice *NativeTun
9 |
10 | type Event int
11 |
12 | const (
13 | EventUp = 1 << iota
14 | EventDown
15 | EventMTUUpdate
16 | )
17 |
18 | type Device interface {
19 | File() *os.File // returns the file descriptor of the device
20 | Read([]byte, int) (int, error) // read a packet from the device (without any additional headers)
21 | Write([]byte, int) (int, error) // writes a packet to the device (without any additional headers)
22 | Flush() error // flush all previous writes to the device
23 | MTU() (int, error) // returns the MTU of the device
24 | Name() (string, error) // fetches and returns the current name
25 | Events() <-chan Event // returns a constant channel of events related to the device
26 | Close() error // stops the device and closes the event channel
27 | }
28 |
--------------------------------------------------------------------------------
/tun/tun_darwin.go:
--------------------------------------------------------------------------------
1 | /* SPDX-License-Identifier: MIT
2 | *
3 | * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
4 | */
5 |
6 | package tun
7 |
8 | import (
9 | "errors"
10 | "fmt"
11 | "net"
12 | "os"
13 | "sync"
14 | "syscall"
15 | "time"
16 | "unsafe"
17 |
18 | "golang.org/x/net/ipv6"
19 | "golang.org/x/sys/unix"
20 | )
21 |
22 | const utunControlName = "com.apple.net.utun_control"
23 |
24 | type NativeTun struct {
25 | name string
26 | tunFile *os.File
27 | events chan Event
28 | errors chan error
29 | routeSocket int
30 | closeOnce sync.Once
31 | }
32 |
33 | func retryInterfaceByIndex(index int) (iface *net.Interface, err error) {
34 | for i := 0; i < 20; i++ {
35 | iface, err = net.InterfaceByIndex(index)
36 | if err != nil && errors.Is(err, syscall.ENOMEM) {
37 | time.Sleep(time.Duration(i) * time.Second / 3)
38 | continue
39 | }
40 | return iface, err
41 | }
42 | return nil, err
43 | }
44 |
45 | func (tun *NativeTun) routineRouteListener(tunIfindex int) {
46 | var (
47 | statusUp bool
48 | statusMTU int
49 | )
50 |
51 | defer close(tun.events)
52 |
53 | data := make([]byte, os.Getpagesize())
54 | for {
55 | retry:
56 | n, err := unix.Read(tun.routeSocket, data)
57 | if err != nil {
58 | if errno, ok := err.(syscall.Errno); ok && errno == syscall.EINTR {
59 | goto retry
60 | }
61 | tun.errors <- err
62 | return
63 | }
64 |
65 | if n < 14 {
66 | continue
67 | }
68 |
69 | if data[3 /* type */] != unix.RTM_IFINFO {
70 | continue
71 | }
72 | ifindex := int(*(*uint16)(unsafe.Pointer(&data[12 /* ifindex */])))
73 | if ifindex != tunIfindex {
74 | continue
75 | }
76 |
77 | iface, err := retryInterfaceByIndex(ifindex)
78 | if err != nil {
79 | tun.errors <- err
80 | return
81 | }
82 |
83 | // Up / Down event
84 | up := (iface.Flags & net.FlagUp) != 0
85 | if up != statusUp && up {
86 | tun.events <- EventUp
87 | }
88 | if up != statusUp && !up {
89 | tun.events <- EventDown
90 | }
91 | statusUp = up
92 |
93 | // MTU changes
94 | if iface.MTU != statusMTU {
95 | tun.events <- EventMTUUpdate
96 | }
97 | statusMTU = iface.MTU
98 | }
99 | }
100 |
101 | func CreateTUN(name string, mtu int) (Device, error) {
102 | ifIndex := -1
103 | if name != "utun" {
104 | _, err := fmt.Sscanf(name, "utun%d", &ifIndex)
105 | if err != nil || ifIndex < 0 {
106 | return nil, fmt.Errorf("Interface name must be utun[0-9]*")
107 | }
108 | }
109 |
110 | fd, err := socketCloexec(unix.AF_SYSTEM, unix.SOCK_DGRAM, 2)
111 | if err != nil {
112 | return nil, err
113 | }
114 |
115 | ctlInfo := &unix.CtlInfo{}
116 | copy(ctlInfo.Name[:], utunControlName)
117 | err = unix.IoctlCtlInfo(fd, ctlInfo)
118 | if err != nil {
119 | unix.Close(fd)
120 | return nil, fmt.Errorf("IoctlGetCtlInfo: %w", err)
121 | }
122 |
123 | sc := &unix.SockaddrCtl{
124 | ID: ctlInfo.Id,
125 | Unit: uint32(ifIndex) + 1,
126 | }
127 |
128 | err = unix.Connect(fd, sc)
129 | if err != nil {
130 | unix.Close(fd)
131 | return nil, err
132 | }
133 |
134 | err = unix.SetNonblock(fd, true)
135 | if err != nil {
136 | unix.Close(fd)
137 | return nil, err
138 | }
139 | tun, err := CreateTUNFromFile(fd, mtu)
140 |
141 | if err == nil && name == "utun" {
142 | fname := os.Getenv("WG_TUN_NAME_FILE")
143 | if fname != "" {
144 | os.WriteFile(fname, []byte(tun.(*NativeTun).name+"\n"), 0o400)
145 | }
146 | }
147 |
148 | return tun, err
149 | }
150 |
151 | func CreateTUNFromFile(fd int, mtu int) (Device, error) {
152 | file := os.NewFile(uintptr(fd), "")
153 | tun := &NativeTun{
154 | tunFile: file,
155 | events: make(chan Event, 10),
156 | errors: make(chan error, 5),
157 | }
158 |
159 | name, err := tun.Name()
160 | if err != nil {
161 | tun.tunFile.Close()
162 | return nil, err
163 | }
164 |
165 | tunIfindex, err := func() (int, error) {
166 | iface, err := net.InterfaceByName(name)
167 | if err != nil {
168 | return -1, err
169 | }
170 | return iface.Index, nil
171 | }()
172 | if err != nil {
173 | tun.tunFile.Close()
174 | return nil, err
175 | }
176 |
177 | tun.routeSocket, err = socketCloexec(unix.AF_ROUTE, unix.SOCK_RAW, unix.AF_UNSPEC)
178 | if err != nil {
179 | tun.tunFile.Close()
180 | return nil, err
181 | }
182 |
183 | go tun.routineRouteListener(tunIfindex)
184 |
185 | if mtu > 0 {
186 | err = tun.setMTU(mtu)
187 | if err != nil {
188 | tun.Close()
189 | return nil, err
190 | }
191 | }
192 |
193 | return tun, nil
194 | }
195 |
196 | func (tun *NativeTun) Name() (string, error) {
197 | var err error
198 | tun.operateOnFd(func(fd uintptr) {
199 | tun.name, err = unix.GetsockoptString(
200 | int(fd),
201 | 2, /* #define SYSPROTO_CONTROL 2 */
202 | 2, /* #define UTUN_OPT_IFNAME 2 */
203 | )
204 | })
205 |
206 | if err != nil {
207 | return "", fmt.Errorf("GetSockoptString: %w", err)
208 | }
209 |
210 | return tun.name, nil
211 | }
212 |
213 | func (tun *NativeTun) File() *os.File {
214 | return tun.tunFile
215 | }
216 |
217 | func (tun *NativeTun) Events() <-chan Event {
218 | return tun.events
219 | }
220 |
221 | func (tun *NativeTun) Read(buff []byte, offset int) (int, error) {
222 | select {
223 | case err := <-tun.errors:
224 | return 0, err
225 | default:
226 | buff := buff[offset-4:]
227 | n, err := tun.tunFile.Read(buff[:])
228 | if n < 4 {
229 | return 0, err
230 | }
231 | return n - 4, err
232 | }
233 | }
234 |
235 | func (tun *NativeTun) Write(buff []byte, offset int) (int, error) {
236 | // reserve space for header
237 |
238 | buff = buff[offset-4:]
239 |
240 | // add packet information header
241 |
242 | buff[0] = 0x00
243 | buff[1] = 0x00
244 | buff[2] = 0x00
245 |
246 | if buff[4]>>4 == ipv6.Version {
247 | buff[3] = unix.AF_INET6
248 | } else {
249 | buff[3] = unix.AF_INET
250 | }
251 |
252 | // write
253 |
254 | return tun.tunFile.Write(buff)
255 | }
256 |
257 | func (tun *NativeTun) Flush() error {
258 | // TODO: can flushing be implemented by buffering and using sendmmsg?
259 | return nil
260 | }
261 |
262 | func (tun *NativeTun) Close() error {
263 | var err1, err2 error
264 | tun.closeOnce.Do(func() {
265 | err1 = tun.tunFile.Close()
266 | if tun.routeSocket != -1 {
267 | unix.Shutdown(tun.routeSocket, unix.SHUT_RDWR)
268 | err2 = unix.Close(tun.routeSocket)
269 | } else if tun.events != nil {
270 | close(tun.events)
271 | }
272 | })
273 | if err1 != nil {
274 | return err1
275 | }
276 | return err2
277 | }
278 |
279 | func (tun *NativeTun) setMTU(n int) error {
280 | fd, err := socketCloexec(
281 | unix.AF_INET,
282 | unix.SOCK_DGRAM,
283 | 0,
284 | )
285 | if err != nil {
286 | return err
287 | }
288 |
289 | defer unix.Close(fd)
290 |
291 | var ifr unix.IfreqMTU
292 | copy(ifr.Name[:], tun.name)
293 | ifr.MTU = int32(n)
294 | err = unix.IoctlSetIfreqMTU(fd, &ifr)
295 | if err != nil {
296 | return fmt.Errorf("failed to set MTU on %s: %w", tun.name, err)
297 | }
298 |
299 | return nil
300 | }
301 |
302 | func (tun *NativeTun) MTU() (int, error) {
303 | fd, err := socketCloexec(
304 | unix.AF_INET,
305 | unix.SOCK_DGRAM,
306 | 0,
307 | )
308 | if err != nil {
309 | return 0, err
310 | }
311 |
312 | defer unix.Close(fd)
313 |
314 | ifr, err := unix.IoctlGetIfreqMTU(fd, tun.name)
315 | if err != nil {
316 | return 0, fmt.Errorf("failed to get MTU on %s: %w", tun.name, err)
317 | }
318 |
319 | return int(ifr.MTU), nil
320 | }
321 |
322 | func socketCloexec(family, sotype, proto int) (fd int, err error) {
323 | // See go/src/net/sys_cloexec.go for background.
324 | syscall.ForkLock.RLock()
325 | defer syscall.ForkLock.RUnlock()
326 |
327 | fd, err = unix.Socket(family, sotype, proto)
328 | if err == nil {
329 | unix.CloseOnExec(fd)
330 | }
331 | return
332 | }
333 |
334 | func (tun *NativeTun) operateOnFd(fn func(fd uintptr)) {
335 | sysconn, err := tun.tunFile.SyscallConn()
336 | if err != nil {
337 | tun.errors <- fmt.Errorf("unable to find sysconn for tunfile: %s", err.Error())
338 | return
339 | }
340 | err = sysconn.Control(fn)
341 | if err != nil {
342 | tun.errors <- fmt.Errorf("unable to control sysconn for tunfile: %s", err.Error())
343 | }
344 | }
345 |
--------------------------------------------------------------------------------
/tun/tun_linux.go:
--------------------------------------------------------------------------------
1 | /* SPDX-License-Identifier: MIT
2 | *
3 | * Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
4 | */
5 |
6 | package tun
7 |
8 | /* Implementation of the TUN device interface for linux
9 | */
10 |
11 | import (
12 | "errors"
13 | "fmt"
14 | "os"
15 | "sync"
16 | "syscall"
17 | "time"
18 | "unsafe"
19 |
20 | "golang.org/x/net/ipv6"
21 | "golang.org/x/sys/unix"
22 | "sslcon/tun/rwcancel"
23 | )
24 |
25 | const (
26 | cloneDevicePath = "/dev/net/tun"
27 | ifReqSize = unix.IFNAMSIZ + 64
28 | )
29 |
30 | type NativeTun struct {
31 | tunFile *os.File
32 | index int32 // if index
33 | errors chan error // async error handling
34 | events chan Event // device related events
35 | nopi bool // the device was passed IFF_NO_PI
36 | netlinkSock int
37 | netlinkCancel *rwcancel.RWCancel
38 | hackListenerClosed sync.Mutex
39 | statusListenersShutdown chan struct{}
40 |
41 | closeOnce sync.Once
42 |
43 | nameOnce sync.Once // guards calling initNameCache, which sets following fields
44 | nameCache string // name of interface
45 | nameErr error
46 | }
47 |
48 | func (tun *NativeTun) File() *os.File {
49 | return tun.tunFile
50 | }
51 |
52 | func (tun *NativeTun) routineHackListener() {
53 | defer tun.hackListenerClosed.Unlock()
54 | /* This is needed for the detection to work across network namespaces
55 | * If you are reading this and know a better method, please get in touch.
56 | */
57 | last := 0
58 | const (
59 | up = 1
60 | down = 2
61 | )
62 | for {
63 | sysconn, err := tun.tunFile.SyscallConn()
64 | if err != nil {
65 | return
66 | }
67 | err2 := sysconn.Control(func(fd uintptr) {
68 | _, err = unix.Write(int(fd), nil)
69 | })
70 | if err2 != nil {
71 | return
72 | }
73 | switch err {
74 | case unix.EINVAL:
75 | if last != up {
76 | // If the tunnel is up, it reports that write() is
77 | // allowed but we provided invalid data.
78 | tun.events <- EventUp
79 | last = up
80 | }
81 | case unix.EIO:
82 | if last != down {
83 | // If the tunnel is down, it reports that no I/O
84 | // is possible, without checking our provided data.
85 | tun.events <- EventDown
86 | last = down
87 | }
88 | default:
89 | return
90 | }
91 | select {
92 | case <-time.After(time.Second):
93 | // nothing
94 | case <-tun.statusListenersShutdown:
95 | return
96 | }
97 | }
98 | }
99 |
100 | func createNetlinkSocket() (int, error) {
101 | sock, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW|unix.SOCK_CLOEXEC, unix.NETLINK_ROUTE)
102 | if err != nil {
103 | return -1, err
104 | }
105 | saddr := &unix.SockaddrNetlink{
106 | Family: unix.AF_NETLINK,
107 | Groups: unix.RTMGRP_LINK | unix.RTMGRP_IPV4_IFADDR | unix.RTMGRP_IPV6_IFADDR,
108 | }
109 | err = unix.Bind(sock, saddr)
110 | if err != nil {
111 | return -1, err
112 | }
113 | return sock, nil
114 | }
115 |
116 | func (tun *NativeTun) routineNetlinkListener() {
117 | defer func() {
118 | unix.Close(tun.netlinkSock)
119 | tun.hackListenerClosed.Lock()
120 | close(tun.events)
121 | tun.netlinkCancel.Close()
122 | }()
123 |
124 | for msg := make([]byte, 1<<16); ; {
125 | var err error
126 | var msgn int
127 | for {
128 | msgn, _, _, _, err = unix.Recvmsg(tun.netlinkSock, msg[:], nil, 0)
129 | if err == nil || !rwcancel.RetryAfterError(err) {
130 | break
131 | }
132 | if !tun.netlinkCancel.ReadyRead() {
133 | tun.errors <- fmt.Errorf("netlink socket closed: %w", err)
134 | return
135 | }
136 | }
137 | if err != nil {
138 | tun.errors <- fmt.Errorf("failed to receive netlink message: %w", err)
139 | return
140 | }
141 |
142 | select {
143 | case <-tun.statusListenersShutdown:
144 | return
145 | default:
146 | }
147 |
148 | wasEverUp := false
149 | for remain := msg[:msgn]; len(remain) >= unix.SizeofNlMsghdr; {
150 |
151 | hdr := *(*unix.NlMsghdr)(unsafe.Pointer(&remain[0]))
152 |
153 | if int(hdr.Len) > len(remain) {
154 | break
155 | }
156 |
157 | switch hdr.Type {
158 | case unix.NLMSG_DONE:
159 | remain = []byte{}
160 |
161 | case unix.RTM_NEWLINK:
162 | info := *(*unix.IfInfomsg)(unsafe.Pointer(&remain[unix.SizeofNlMsghdr]))
163 | remain = remain[hdr.Len:]
164 |
165 | if info.Index != tun.index {
166 | // not our interface
167 | continue
168 | }
169 |
170 | if info.Flags&unix.IFF_RUNNING != 0 {
171 | tun.events <- EventUp
172 | wasEverUp = true
173 | }
174 |
175 | if info.Flags&unix.IFF_RUNNING == 0 {
176 | // Don't emit EventDown before we've ever emitted EventUp.
177 | // This avoids a startup race with HackListener, which
178 | // might detect Up before we have finished reporting Down.
179 | if wasEverUp {
180 | tun.events <- EventDown
181 | }
182 | }
183 |
184 | tun.events <- EventMTUUpdate
185 |
186 | default:
187 | remain = remain[hdr.Len:]
188 | }
189 | }
190 | }
191 | }
192 |
193 | func getIFIndex(name string) (int32, error) {
194 | fd, err := unix.Socket(
195 | unix.AF_INET,
196 | unix.SOCK_DGRAM|unix.SOCK_CLOEXEC,
197 | 0,
198 | )
199 | if err != nil {
200 | return 0, err
201 | }
202 |
203 | defer unix.Close(fd)
204 |
205 | var ifr [ifReqSize]byte
206 | copy(ifr[:], name)
207 | _, _, errno := unix.Syscall(
208 | unix.SYS_IOCTL,
209 | uintptr(fd),
210 | uintptr(unix.SIOCGIFINDEX),
211 | uintptr(unsafe.Pointer(&ifr[0])),
212 | )
213 |
214 | if errno != 0 {
215 | return 0, errno
216 | }
217 |
218 | return *(*int32)(unsafe.Pointer(&ifr[unix.IFNAMSIZ])), nil
219 | }
220 |
221 | func (tun *NativeTun) setMTU(n int) error {
222 | name, err := tun.Name()
223 | if err != nil {
224 | return err
225 | }
226 |
227 | // open datagram socket
228 | fd, err := unix.Socket(
229 | unix.AF_INET,
230 | unix.SOCK_DGRAM|unix.SOCK_CLOEXEC,
231 | 0,
232 | )
233 | if err != nil {
234 | return err
235 | }
236 |
237 | defer unix.Close(fd)
238 |
239 | // do ioctl call
240 | var ifr [ifReqSize]byte
241 | copy(ifr[:], name)
242 | *(*uint32)(unsafe.Pointer(&ifr[unix.IFNAMSIZ])) = uint32(n)
243 | _, _, errno := unix.Syscall(
244 | unix.SYS_IOCTL,
245 | uintptr(fd),
246 | uintptr(unix.SIOCSIFMTU),
247 | uintptr(unsafe.Pointer(&ifr[0])),
248 | )
249 |
250 | if errno != 0 {
251 | return fmt.Errorf("failed to set MTU of TUN device: %w", errno)
252 | }
253 |
254 | return nil
255 | }
256 |
257 | func (tun *NativeTun) MTU() (int, error) {
258 | name, err := tun.Name()
259 | if err != nil {
260 | return 0, err
261 | }
262 |
263 | // open datagram socket
264 | fd, err := unix.Socket(
265 | unix.AF_INET,
266 | unix.SOCK_DGRAM|unix.SOCK_CLOEXEC,
267 | 0,
268 | )
269 | if err != nil {
270 | return 0, err
271 | }
272 |
273 | defer unix.Close(fd)
274 |
275 | // do ioctl call
276 |
277 | var ifr [ifReqSize]byte
278 | copy(ifr[:], name)
279 | _, _, errno := unix.Syscall(
280 | unix.SYS_IOCTL,
281 | uintptr(fd),
282 | uintptr(unix.SIOCGIFMTU),
283 | uintptr(unsafe.Pointer(&ifr[0])),
284 | )
285 | if errno != 0 {
286 | return 0, fmt.Errorf("failed to get MTU of TUN device: %w", errno)
287 | }
288 |
289 | return int(*(*int32)(unsafe.Pointer(&ifr[unix.IFNAMSIZ]))), nil
290 | }
291 |
292 | func (tun *NativeTun) Name() (string, error) {
293 | tun.nameOnce.Do(tun.initNameCache)
294 | return tun.nameCache, tun.nameErr
295 | }
296 |
297 | func (tun *NativeTun) initNameCache() {
298 | tun.nameCache, tun.nameErr = tun.nameSlow()
299 | }
300 |
301 | func (tun *NativeTun) nameSlow() (string, error) {
302 | sysconn, err := tun.tunFile.SyscallConn()
303 | if err != nil {
304 | return "", err
305 | }
306 | var ifr [ifReqSize]byte
307 | var errno syscall.Errno
308 | err = sysconn.Control(func(fd uintptr) {
309 | _, _, errno = unix.Syscall(
310 | unix.SYS_IOCTL,
311 | fd,
312 | uintptr(unix.TUNGETIFF),
313 | uintptr(unsafe.Pointer(&ifr[0])),
314 | )
315 | })
316 | if err != nil {
317 | return "", fmt.Errorf("failed to get name of TUN device: %w", err)
318 | }
319 | if errno != 0 {
320 | return "", fmt.Errorf("failed to get name of TUN device: %w", errno)
321 | }
322 | return unix.ByteSliceToString(ifr[:]), nil
323 | }
324 |
325 | func (tun *NativeTun) Write(buf []byte, offset int) (int, error) {
326 | if tun.nopi {
327 | buf = buf[offset:]
328 | } else {
329 | // reserve space for header
330 | buf = buf[offset-4:]
331 |
332 | // add packet information header
333 | buf[0] = 0x00
334 | buf[1] = 0x00
335 | if buf[4]>>4 == ipv6.Version {
336 | buf[2] = 0x86
337 | buf[3] = 0xdd
338 | } else {
339 | buf[2] = 0x08
340 | buf[3] = 0x00
341 | }
342 | }
343 |
344 | n, err := tun.tunFile.Write(buf)
345 | if errors.Is(err, syscall.EBADFD) {
346 | err = os.ErrClosed
347 | }
348 | return n, err
349 | }
350 |
351 | func (tun *NativeTun) Flush() error {
352 | // TODO: can flushing be implemented by buffering and using sendmmsg?
353 | return nil
354 | }
355 |
356 | func (tun *NativeTun) Read(buf []byte, offset int) (n int, err error) {
357 | select {
358 | case err = <-tun.errors:
359 | default:
360 | if tun.nopi {
361 | n, err = tun.tunFile.Read(buf[offset:])
362 | } else {
363 | buff := buf[offset-4:]
364 | n, err = tun.tunFile.Read(buff[:])
365 | if errors.Is(err, syscall.EBADFD) {
366 | err = os.ErrClosed
367 | }
368 | if n < 4 {
369 | n = 0
370 | } else {
371 | n -= 4
372 | }
373 | }
374 | }
375 | return
376 | }
377 |
378 | func (tun *NativeTun) Events() <-chan Event {
379 | return tun.events
380 | }
381 |
382 | func (tun *NativeTun) Close() error {
383 | var err1, err2 error
384 | tun.closeOnce.Do(func() {
385 | if tun.statusListenersShutdown != nil {
386 | close(tun.statusListenersShutdown)
387 | if tun.netlinkCancel != nil {
388 | err1 = tun.netlinkCancel.Cancel()
389 | }
390 | } else if tun.events != nil {
391 | close(tun.events)
392 | }
393 | err2 = tun.tunFile.Close()
394 | })
395 | if err1 != nil {
396 | return err1
397 | }
398 | return err2
399 | }
400 |
401 | func CreateTUN(name string, mtu int) (Device, error) {
402 | nfd, err := unix.Open(cloneDevicePath, unix.O_RDWR|unix.O_CLOEXEC, 0)
403 | if err != nil {
404 | if os.IsNotExist(err) {
405 | return nil, fmt.Errorf("CreateTUN(%q) failed; %s does not exist", name, cloneDevicePath)
406 | }
407 | return nil, err
408 | }
409 |
410 | var ifr [ifReqSize]byte
411 | var flags uint16 = unix.IFF_TUN | unix.IFF_NO_PI // (disabled for TUN status hack)
412 | nameBytes := []byte(name)
413 | if len(nameBytes) >= unix.IFNAMSIZ {
414 | unix.Close(nfd)
415 | return nil, fmt.Errorf("interface name too long: %w", unix.ENAMETOOLONG)
416 | }
417 | copy(ifr[:], nameBytes)
418 | *(*uint16)(unsafe.Pointer(&ifr[unix.IFNAMSIZ])) = flags
419 |
420 | _, _, errno := unix.Syscall(
421 | unix.SYS_IOCTL,
422 | uintptr(nfd),
423 | uintptr(unix.TUNSETIFF),
424 | uintptr(unsafe.Pointer(&ifr[0])),
425 | )
426 | if errno != 0 {
427 | unix.Close(nfd)
428 | return nil, errno
429 | }
430 |
431 | err = unix.SetNonblock(nfd, true)
432 | if err != nil {
433 | unix.Close(nfd)
434 | return nil, err
435 | }
436 |
437 | // Note that the above -- open,ioctl,nonblock -- must happen prior to handing it to netpoll as below this line.
438 |
439 | fd := os.NewFile(uintptr(nfd), cloneDevicePath)
440 | return CreateTUNFromFile(fd, mtu)
441 | }
442 |
443 | func CreateTUNFromFile(file *os.File, mtu int) (Device, error) {
444 | tun := &NativeTun{
445 | tunFile: file,
446 | events: make(chan Event, 5),
447 | errors: make(chan error, 5),
448 | statusListenersShutdown: make(chan struct{}),
449 | nopi: true,
450 | }
451 |
452 | name, err := tun.Name()
453 | if err != nil {
454 | return nil, err
455 | }
456 |
457 | // start event listener
458 |
459 | tun.index, err = getIFIndex(name)
460 | if err != nil {
461 | return nil, err
462 | }
463 |
464 | tun.netlinkSock, err = createNetlinkSocket()
465 | if err != nil {
466 | return nil, err
467 | }
468 | tun.netlinkCancel, err = rwcancel.NewRWCancel(tun.netlinkSock)
469 | if err != nil {
470 | unix.Close(tun.netlinkSock)
471 | return nil, err
472 | }
473 |
474 | tun.hackListenerClosed.Lock()
475 | go tun.routineNetlinkListener()
476 | go tun.routineHackListener() // cross namespace
477 |
478 | err = tun.setMTU(mtu)
479 | if err != nil {
480 | unix.Close(tun.netlinkSock)
481 | return nil, err
482 | }
483 |
484 | return tun, nil
485 | }
486 |
487 | func CreateUnmonitoredTUNFromFD(fd int) (Device, string, error) {
488 | err := unix.SetNonblock(fd, true)
489 | if err != nil {
490 | return nil, "", err
491 | }
492 | file := os.NewFile(uintptr(fd), "/dev/tun")
493 | tun := &NativeTun{
494 | tunFile: file,
495 | events: make(chan Event, 5),
496 | errors: make(chan error, 5),
497 | nopi: true,
498 | }
499 | name, err := tun.Name()
500 | if err != nil {
501 | return nil, "", err
502 | }
503 | return tun, name, nil
504 | }
505 |
--------------------------------------------------------------------------------
/tun/tun_windows.go:
--------------------------------------------------------------------------------
1 | package tun
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "os"
8 | "sync"
9 | "sync/atomic"
10 |
11 | "github.com/lysShub/wintun-go"
12 | "golang.org/x/sys/windows"
13 | "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
14 | )
15 |
16 | type NativeTun struct {
17 | wt *wintun.Adapter
18 | name string
19 | mtu int
20 |
21 | closeOnce sync.Once
22 | close atomic.Bool
23 | }
24 |
25 | var (
26 | WintunTunnelType = "TLSLink Secure"
27 | WintunStaticRequestedGUID = &windows.GUID{
28 | 0x0000000,
29 | 0xFFFF,
30 | 0xFFFF,
31 | [8]byte{0xFF, 0xe9, 0x76, 0xe5, 0x8c, 0x74, 0x06, 0x3e},
32 | }
33 | )
34 |
35 | func init() {
36 | wintun.MustLoad(wintun.DLL)
37 | }
38 |
39 | func CreateTUN(ifname string, mtu int) (Device, error) {
40 | wt, err := wintun.CreateAdapter(ifname,
41 | wintun.TunType(WintunTunnelType),
42 | wintun.Guid(WintunStaticRequestedGUID),
43 | wintun.RingBuff(0x800000)) // 8 MiB, 5个 0 为 1 MiB
44 |
45 | tun := &NativeTun{
46 | wt: wt,
47 | name: ifname,
48 | mtu: mtu,
49 | }
50 |
51 | return tun, err
52 | }
53 |
54 | func (tun *NativeTun) File() *os.File {
55 | return nil
56 | }
57 |
58 | func (tun *NativeTun) Read(buff []byte, offset int) (int, error) {
59 |
60 | if tun.close.Load() {
61 | return 0, os.ErrClosed
62 | }
63 |
64 | for {
65 | packet, err := tun.wt.Recv(context.Background())
66 | switch err {
67 | case nil:
68 | packetSize := len(packet)
69 | copy(buff[offset:], packet)
70 | tun.wt.Release(packet)
71 | // tun.rate.update(uint64(packetSize))
72 | return packetSize, nil
73 | case windows.ERROR_HANDLE_EOF:
74 | return 0, os.ErrClosed
75 | case windows.ERROR_INVALID_DATA:
76 | return 0, errors.New("send ring corrupt")
77 | }
78 | return 0, fmt.Errorf("read failed: %w", err)
79 | }
80 | }
81 |
82 | func (tun *NativeTun) Write(buff []byte, offset int) (int, error) {
83 |
84 | if tun.close.Load() {
85 | return 0, os.ErrClosed
86 | }
87 |
88 | packetSize := len(buff) - offset
89 |
90 | packet, err := tun.wt.Alloc(packetSize)
91 | if err == nil {
92 | copy(packet, buff[offset:])
93 | err = tun.wt.Send(packet)
94 | if err != nil {
95 | return 0, err
96 | }
97 | return int(packetSize), nil
98 | }
99 | switch err {
100 | case windows.ERROR_HANDLE_EOF:
101 | return 0, os.ErrClosed
102 | case windows.ERROR_BUFFER_OVERFLOW:
103 | return 0, nil // Dropping when ring is full.
104 | }
105 | return 0, fmt.Errorf("write failed: %w", err)
106 | }
107 |
108 | func (tun *NativeTun) Flush() error {
109 | return nil
110 | }
111 |
112 | func (tun *NativeTun) MTU() (int, error) {
113 | return tun.mtu, nil
114 | }
115 |
116 | func (tun *NativeTun) Name() (string, error) {
117 | return tun.name, nil
118 | }
119 |
120 | func (tun *NativeTun) Events() <-chan Event {
121 | return nil
122 | }
123 |
124 | func (tun *NativeTun) Close() error {
125 | tun.closeOnce.Do(func() {
126 | tun.close.Store(true)
127 |
128 | if tun.wt != nil {
129 | tun.wt.Close()
130 | }
131 | })
132 |
133 | return nil
134 | }
135 |
136 | func (tun *NativeTun) LUID() winipcfg.LUID {
137 | luid, err := tun.wt.GetAdapterLuid()
138 | if err != nil {
139 | return 0
140 | }
141 | return luid
142 | }
143 |
--------------------------------------------------------------------------------
/utils/record.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "os"
7 | )
8 |
9 | type Record struct {
10 | Filename string
11 | Contents []string
12 | }
13 |
14 | func NewRecord(filename string) *Record {
15 | return &Record{
16 | Filename: filename,
17 | Contents: make([]string, 0),
18 | }
19 | }
20 |
21 | func (r *Record) readLines() error {
22 | if _, err := os.Stat(r.Filename); err != nil {
23 | return nil
24 | }
25 |
26 | f, err := os.OpenFile(r.Filename, os.O_RDONLY, 0600)
27 | if err != nil {
28 | return err
29 | }
30 | defer f.Close()
31 |
32 | scanner := bufio.NewScanner(f)
33 | for scanner.Scan() {
34 | if tmp := scanner.Text(); len(tmp) != 0 {
35 | r.Contents = append(r.Contents, tmp)
36 | }
37 | }
38 |
39 | return nil
40 | }
41 |
42 | func (r *Record) Write(content string, prepend bool) error {
43 | if prepend {
44 | err := r.readLines()
45 | if err != nil {
46 | return err
47 | }
48 | }
49 |
50 | f, err := os.OpenFile(r.Filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0777)
51 | if err != nil {
52 | return err
53 | }
54 | defer f.Close()
55 |
56 | writer := bufio.NewWriter(f)
57 | writer.WriteString(fmt.Sprintf("%s\n", content))
58 |
59 | if prepend {
60 | for _, line := range r.Contents {
61 | _, err = writer.WriteString(fmt.Sprintf("%s\n", line))
62 | if err != nil {
63 | return err
64 | }
65 | }
66 | }
67 |
68 | if err = writer.Flush(); err != nil {
69 | return err
70 | }
71 |
72 | return nil
73 | }
74 |
--------------------------------------------------------------------------------
/utils/utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "crypto/rand"
5 | "fmt"
6 | "net"
7 | "net/http"
8 | "os"
9 | "regexp"
10 | "runtime"
11 | "strings"
12 |
13 | "github.com/pion/dtls/v3/pkg/protocol"
14 | "sslcon/base"
15 | "sslcon/utils/waterutil"
16 | )
17 |
18 | func InArray(arr []string, str string) bool {
19 | for _, d := range arr {
20 | if d == str {
21 | return true
22 | }
23 | }
24 | return false
25 | }
26 |
27 | func InArrayGeneric(arr []string, str string) bool {
28 | for _, d := range arr {
29 | if d != "" && strings.HasSuffix(str, d) {
30 | return true
31 | }
32 | }
33 | return false
34 | }
35 |
36 | // SetCommonHeader 认证和建立隧道都需要的 HTTP Header
37 | // ocserv worker-http.c case HEADER_USER_AGENT 通过 strncasecmp() 函数比较前 n 个字符
38 | func SetCommonHeader(req *http.Request) {
39 | if base.Cfg.CiscoCompat || base.Cfg.AgentName == "" {
40 | base.Cfg.AgentName = "AnyConnect"
41 | }
42 | req.Header.Set("User-Agent", fmt.Sprintf("%s %s %s", base.Cfg.AgentName, FirstUpper(runtime.GOOS+"_"+runtime.GOARCH), base.Cfg.AgentVersion))
43 | req.Header.Set("Content-Type", "application/xml")
44 | }
45 |
46 | func IpMask2CIDR(ip, mask string) string {
47 | length, _ := net.IPMask(net.ParseIP(mask).To4()).Size()
48 | return fmt.Sprintf("%s/%v", ip, length)
49 | }
50 |
51 | // IpMaskToCIDR 输入 192.168.1.10/255.255.255.255 返回 192.168.1.10/32
52 | func IpMaskToCIDR(ipMask string) string {
53 | ips := strings.Split(ipMask, "/")
54 | length, _ := net.IPMask(net.ParseIP(ips[1]).To4()).Size()
55 | return fmt.Sprintf("%s/%v", ips[0], length)
56 | }
57 |
58 | func ResolvePacket(packet []byte) (string, uint16, string, uint16) {
59 | src := waterutil.IPv4Source(packet)
60 | srcPort := waterutil.IPv4SourcePort(packet)
61 | dst := waterutil.IPv4Destination(packet)
62 | dstPort := waterutil.IPv4DestinationPort(packet)
63 | return src.String(), srcPort, dst.String(), dstPort
64 | }
65 |
66 | func MakeMasterSecret() ([]byte, error) {
67 | masterSecret := make([]byte, 48)
68 | masterSecret[0] = protocol.Version1_2.Major
69 | masterSecret[1] = protocol.Version1_2.Minor
70 | _, err := rand.Read(masterSecret[2:])
71 | return masterSecret, err
72 | }
73 |
74 | func Min(init int, other ...int) int {
75 | minValue := init
76 | for _, val := range other {
77 | if val != 0 && val < minValue {
78 | minValue = val
79 | }
80 | }
81 | return minValue
82 | }
83 |
84 | func Max(init int, other ...int) int {
85 | maxValue := init
86 | for _, val := range other {
87 | if val > maxValue {
88 | maxValue = val
89 | }
90 | }
91 | return maxValue
92 | }
93 |
94 | func CopyFile(dstName, srcName string) (err error) {
95 | input, err := os.ReadFile(srcName)
96 | if err != nil {
97 | return err
98 | }
99 |
100 | err = os.WriteFile(dstName, input, 0644)
101 | if err != nil {
102 | return err
103 | }
104 | return nil
105 | }
106 |
107 | func FirstUpper(s string) string {
108 | if s == "" {
109 | return ""
110 | }
111 | return strings.ToUpper(s[:1]) + s[1:]
112 | }
113 |
114 | func RemoveBetween(input, start, end string) string {
115 | // 构建正则表达式模式,"(?s)" 包括换行符
116 | pattern := "(?s)" + regexp.QuoteMeta(start) + ".*?" + regexp.QuoteMeta(end)
117 | r := regexp.MustCompile(pattern)
118 | return r.ReplaceAllString(input, "")
119 | }
120 |
--------------------------------------------------------------------------------
/utils/vpnc/vpnc_darwin.go:
--------------------------------------------------------------------------------
1 | package vpnc
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "os/exec"
7 | "strings"
8 |
9 | "github.com/jackpal/gateway"
10 | "sslcon/base"
11 | "sslcon/session"
12 | "sslcon/utils"
13 | )
14 |
15 | var VPNAddress string
16 |
17 | func ConfigInterface(cSess *session.ConnSession) error {
18 | VPNAddress = cSess.VPNAddress
19 | cmdStr1 := fmt.Sprintf("ifconfig %s inet %s %s netmask %s up", cSess.TunName, cSess.VPNAddress, cSess.VPNAddress, "255.255.255.255")
20 | err := execCmd([]string{cmdStr1})
21 |
22 | return err
23 | }
24 |
25 | func SetRoutes(cSess *session.ConnSession) error {
26 | cmdStr1 := fmt.Sprintf("route add -host %s %s", cSess.ServerAddress, base.LocalInterface.Gateway)
27 | err := execCmd([]string{cmdStr1})
28 | if err != nil {
29 | return err
30 | }
31 | // 默认路由通过 DNS 设置
32 | if len(cSess.SplitInclude) != 0 {
33 | for _, ipMask := range cSess.SplitInclude {
34 | dst := utils.IpMaskToCIDR(ipMask)
35 | cmdStr := fmt.Sprintf("route add -net %s %s", dst, cSess.VPNAddress)
36 | err = execCmd([]string{cmdStr})
37 | if err != nil {
38 | return routingError(dst, err)
39 | }
40 | }
41 | }
42 |
43 | if len(cSess.SplitExclude) > 0 {
44 | for _, ipMask := range cSess.SplitExclude {
45 | dst := utils.IpMaskToCIDR(ipMask)
46 | cmdStr := fmt.Sprintf("route add -net %s %s", dst, base.LocalInterface.Gateway)
47 | err = execCmd([]string{cmdStr})
48 | if err != nil {
49 | return routingError(dst, err)
50 | }
51 | }
52 | }
53 |
54 | // dns
55 | if len(cSess.DNS) > 0 {
56 | err = setDNS(cSess)
57 | }
58 |
59 | return err
60 | }
61 |
62 | func ResetRoutes(cSess *session.ConnSession) {
63 | // cmdStr1 := fmt.Sprintf("route delete default %s", cSess.VPNAddress)
64 | // cmdStr2 := fmt.Sprintf("route add default %s", base.LocalInterface.Gateway)
65 |
66 | cmdStr3 := fmt.Sprintf("route delete -host %s %s", cSess.ServerAddress, base.LocalInterface.Gateway)
67 | _ = execCmd([]string{cmdStr3})
68 |
69 | if len(cSess.SplitExclude) > 0 {
70 | for _, ipMask := range cSess.SplitExclude {
71 | dst := utils.IpMaskToCIDR(ipMask)
72 | cmdStr := fmt.Sprintf("route delete -net %s %s", dst, base.LocalInterface.Gateway)
73 | _ = execCmd([]string{cmdStr})
74 | }
75 | }
76 |
77 | if len(cSess.DynamicSplitExcludeDomains) > 0 {
78 | cSess.DynamicSplitExcludeResolved.Range(func(_, value any) bool {
79 | ips := value.([]string)
80 | for _, ip := range ips {
81 | dst := ip + "/32"
82 | cmdStr := fmt.Sprintf("route delete -net %s %s", dst, base.LocalInterface.Gateway)
83 | _ = execCmd([]string{cmdStr})
84 | }
85 |
86 | return true
87 | })
88 | }
89 |
90 | if len(cSess.DNS) > 0 {
91 | restoreDNS(cSess)
92 | }
93 | }
94 |
95 | func DynamicAddIncludeRoutes(ips []string) {
96 | for _, ip := range ips {
97 | dst := ip + "/32"
98 | cmdStr := fmt.Sprintf("route add -net %s %s", dst, VPNAddress)
99 | _ = execCmd([]string{cmdStr})
100 | }
101 | }
102 |
103 | func DynamicAddExcludeRoutes(ips []string) {
104 | for _, ip := range ips {
105 | dst := ip + "/32"
106 | cmdStr := fmt.Sprintf("route add -net %s %s", dst, base.LocalInterface.Gateway)
107 | _ = execCmd([]string{cmdStr})
108 | }
109 | }
110 |
111 | func GetLocalInterface() error {
112 | localInterfaceIP, err := gateway.DiscoverInterface()
113 | if err != nil {
114 | return err
115 | }
116 | gateway, err := gateway.DiscoverGateway()
117 | if err != nil {
118 | return err
119 | }
120 |
121 | localInterface := net.Interface{}
122 |
123 | ifaces, _ := net.Interfaces()
124 | for _, iface := range ifaces {
125 | addrs, _ := iface.Addrs()
126 | for _, addr := range addrs {
127 | ipnet, ok := addr.(*net.IPNet)
128 | if !ok {
129 | continue
130 | }
131 |
132 | ip := ipnet.IP.To4()
133 | if ip.Equal(localInterfaceIP) {
134 | localInterface = iface
135 | break
136 | }
137 | }
138 | }
139 |
140 | base.LocalInterface.Name = localInterface.Name
141 | base.LocalInterface.Ip4 = localInterfaceIP.String()
142 | base.LocalInterface.Gateway = gateway.String()
143 | base.LocalInterface.Mac = localInterface.HardwareAddr.String()
144 |
145 | base.Info("GetLocalInterface:", fmt.Sprintf("%+v", *base.LocalInterface))
146 |
147 | return nil
148 | }
149 |
150 | func routingError(dst string, err error) error {
151 | return fmt.Errorf("routing error: %s %s", dst, err)
152 | }
153 |
154 | func execCmd(cmdStrs []string) error {
155 | for _, cmdStr := range cmdStrs {
156 | cmd := exec.Command("sh", "-c", cmdStr)
157 | stdoutStderr, err := cmd.CombinedOutput()
158 | if err != nil {
159 | return fmt.Errorf("%s %s %s", err, cmd.String(), string(stdoutStderr))
160 | }
161 | }
162 | return nil
163 | }
164 |
165 | func setDNS(cSess *session.ConnSession) error {
166 |
167 | if len(cSess.DynamicSplitIncludeDomains) > 0 {
168 | DynamicAddIncludeRoutes(cSess.DNS)
169 | }
170 |
171 | var override string
172 | // 如果包含路由为空必为全局路由,如果使用包含域名,则包含路由必须填写一个,如 dns 地址
173 | if len(cSess.SplitInclude) == 0 {
174 | override = "d.add OverridePrimary # 1"
175 | }
176 |
177 | command := fmt.Sprintf(`
178 | open
179 | d.init
180 | d.add ServerAddresses * %s
181 | d.add SearchOrder 1
182 | d.add SupplementalMatchDomains * ""
183 | set State:/Network/Service/%s/DNS
184 |
185 | d.init
186 | d.add Router %s
187 | d.add Addresses * %s
188 | d.add InterfaceName %s
189 | %s
190 | set State:/Network/Service/%s/IPv4
191 | close
192 | `, strings.Join(cSess.DNS, " "), cSess.TunName, cSess.VPNAddress, cSess.VPNAddress, cSess.TunName, override, cSess.TunName)
193 |
194 | cmd := exec.Command("scutil")
195 | cmd.Stdin = strings.NewReader(command)
196 |
197 | // 执行命令并获取输出
198 | output, err := cmd.CombinedOutput()
199 | if err != nil {
200 | base.Error(err, output)
201 | }
202 | return err
203 | }
204 |
205 | func restoreDNS(cSess *session.ConnSession) {
206 | command := fmt.Sprintf(`
207 | open
208 | remove State:/Network/Service/%s/IPv4
209 | remove State:/Network/Service/%s/DNS
210 | close
211 | `, cSess.TunName, cSess.TunName)
212 |
213 | cmd := exec.Command("scutil")
214 | cmd.Stdin = strings.NewReader(command)
215 |
216 | // 执行命令并获取输出
217 | output, err := cmd.CombinedOutput()
218 | if err != nil {
219 | base.Error(err, output)
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/utils/vpnc/vpnc_linux.go:
--------------------------------------------------------------------------------
1 | package vpnc
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "os/exec"
7 | "strings"
8 | "time"
9 |
10 | "github.com/vishvananda/netlink"
11 | "sslcon/base"
12 | "sslcon/session"
13 | "sslcon/utils"
14 | )
15 |
16 | var (
17 | localInterface netlink.Link
18 | iface netlink.Link
19 | )
20 |
21 | func ConfigInterface(cSess *session.ConnSession) error {
22 | var err error
23 | iface, err = netlink.LinkByName(cSess.TunName)
24 | if err != nil {
25 | return err
26 | }
27 | // ip address
28 | _ = netlink.LinkSetUp(iface)
29 | _ = netlink.LinkSetMulticastOff(iface)
30 |
31 | addr, _ := netlink.ParseAddr(utils.IpMask2CIDR(cSess.VPNAddress, cSess.VPNMask))
32 | err = netlink.AddrAdd(iface, addr)
33 |
34 | return err
35 | }
36 |
37 | func SetRoutes(cSess *session.ConnSession) error {
38 | // routes
39 | dst, _ := netlink.ParseIPNet(cSess.ServerAddress + "/32")
40 | gateway := net.ParseIP(base.LocalInterface.Gateway)
41 |
42 | ifaceIndex := iface.Attrs().Index
43 | localInterfaceIndex := localInterface.Attrs().Index
44 |
45 | route := netlink.Route{LinkIndex: localInterfaceIndex, Dst: dst, Gw: gateway}
46 | err := netlink.RouteAdd(&route)
47 | if err != nil {
48 | if !strings.HasSuffix(err.Error(), "exists") {
49 | return routingError(dst, err)
50 | }
51 | }
52 |
53 | // 如果包含路由为空必为全局路由,如果使用包含域名,则包含路由必须填写一个,如 dns 地址
54 | if len(cSess.SplitInclude) == 0 {
55 | cSess.SplitInclude = append(cSess.SplitInclude, "0.0.0.0/0.0.0.0")
56 |
57 | // 全局模式,重置默认路由优先级,如 OpenWrt 默认优先级为 0
58 | zero, _ := netlink.ParseIPNet("0.0.0.0/0")
59 | delAllRoute(&netlink.Route{LinkIndex: localInterfaceIndex, Dst: zero})
60 | _ = netlink.RouteAdd(&netlink.Route{LinkIndex: localInterfaceIndex, Dst: zero, Gw: gateway, Priority: 10})
61 | }
62 |
63 | // 如果使用域名包含,原则上不支持在顶级域名匹配中排除某个具体域名的 IP
64 | for _, ipMask := range cSess.SplitInclude {
65 | dst, _ = netlink.ParseIPNet(utils.IpMaskToCIDR(ipMask))
66 | route = netlink.Route{LinkIndex: ifaceIndex, Dst: dst, Priority: 6}
67 | err = netlink.RouteAdd(&route)
68 | if err != nil {
69 | if !strings.HasSuffix(err.Error(), "exists") {
70 | return routingError(dst, err)
71 | }
72 | }
73 | }
74 |
75 | // 支持在 SplitInclude 网段中排除某个路由
76 | if len(cSess.SplitExclude) > 0 {
77 | for _, ipMask := range cSess.SplitExclude {
78 | dst, _ = netlink.ParseIPNet(utils.IpMaskToCIDR(ipMask))
79 | route = netlink.Route{LinkIndex: localInterfaceIndex, Dst: dst, Gw: gateway, Priority: 5}
80 | err = netlink.RouteAdd(&route)
81 | if err != nil {
82 | if !strings.HasSuffix(err.Error(), "exists") {
83 | return routingError(dst, err)
84 | }
85 | }
86 | }
87 | }
88 |
89 | if len(cSess.DNS) > 0 {
90 | setDNS(cSess)
91 | }
92 |
93 | return nil
94 | }
95 |
96 | func ResetRoutes(cSess *session.ConnSession) {
97 | // routes
98 | localInterfaceIndex := localInterface.Attrs().Index
99 |
100 | for _, ipMask := range cSess.SplitInclude {
101 | if ipMask == "0.0.0.0/0.0.0.0" {
102 | // 重置默认路由优先级
103 | zero, _ := netlink.ParseIPNet("0.0.0.0/0")
104 | gateway := net.ParseIP(base.LocalInterface.Gateway)
105 | _ = netlink.RouteDel(&netlink.Route{LinkIndex: localInterfaceIndex, Dst: zero})
106 | _ = netlink.RouteAdd(&netlink.Route{LinkIndex: localInterfaceIndex, Dst: zero, Gw: gateway})
107 | break
108 | }
109 | }
110 |
111 | dst, _ := netlink.ParseIPNet(cSess.ServerAddress + "/32")
112 | _ = netlink.RouteDel(&netlink.Route{LinkIndex: localInterfaceIndex, Dst: dst})
113 |
114 | if len(cSess.SplitExclude) > 0 {
115 | for _, ipMask := range cSess.SplitExclude {
116 | dst, _ = netlink.ParseIPNet(utils.IpMaskToCIDR(ipMask))
117 | _ = netlink.RouteDel(&netlink.Route{LinkIndex: localInterfaceIndex, Dst: dst})
118 | }
119 | }
120 |
121 | if len(cSess.DynamicSplitExcludeDomains) > 0 {
122 | cSess.DynamicSplitExcludeResolved.Range(func(_, value any) bool {
123 | ips := value.([]string)
124 | for _, ip := range ips {
125 | dst, _ = netlink.ParseIPNet(ip + "/32")
126 | _ = netlink.RouteDel(&netlink.Route{LinkIndex: localInterfaceIndex, Dst: dst})
127 | }
128 |
129 | return true
130 | })
131 | }
132 |
133 | if len(cSess.DNS) > 0 {
134 | restoreDNS(cSess)
135 | }
136 | }
137 |
138 | func DynamicAddIncludeRoutes(ips []string) {
139 | ifaceIndex := iface.Attrs().Index
140 |
141 | for _, ip := range ips {
142 | dst, _ := netlink.ParseIPNet(ip + "/32")
143 | route := netlink.Route{LinkIndex: ifaceIndex, Dst: dst, Priority: 6}
144 | _ = netlink.RouteAdd(&route)
145 | }
146 | }
147 |
148 | func DynamicAddExcludeRoutes(ips []string) {
149 | localInterfaceIndex := localInterface.Attrs().Index
150 | gateway := net.ParseIP(base.LocalInterface.Gateway)
151 |
152 | for _, ip := range ips {
153 | dst, _ := netlink.ParseIPNet(ip + "/32")
154 | route := netlink.Route{LinkIndex: localInterfaceIndex, Dst: dst, Gw: gateway, Priority: 5}
155 | _ = netlink.RouteAdd(&route)
156 | }
157 | }
158 |
159 | func GetLocalInterface() error {
160 |
161 | // just for default route
162 | routes, err := netlink.RouteGet(net.ParseIP("8.8.8.8"))
163 | if len(routes) > 0 {
164 | route := routes[0]
165 | localInterface, err = netlink.LinkByIndex(route.LinkIndex)
166 | if err != nil {
167 | return err
168 | }
169 | base.LocalInterface.Name = localInterface.Attrs().Name
170 | base.LocalInterface.Ip4 = route.Src.String()
171 | base.LocalInterface.Gateway = route.Gw.String()
172 | base.LocalInterface.Mac = localInterface.Attrs().HardwareAddr.String()
173 |
174 | base.Info("GetLocalInterface:", fmt.Sprintf("%+v", *base.LocalInterface))
175 | return nil
176 | }
177 | return err
178 | }
179 |
180 | func delAllRoute(route *netlink.Route) {
181 | err := netlink.RouteDel(route)
182 | if err != nil {
183 | return
184 | }
185 | delAllRoute(route)
186 | }
187 |
188 | func routingError(dst *net.IPNet, err error) error {
189 | return fmt.Errorf("routing error: %s %s", dst.String(), err)
190 | }
191 |
192 | func setDNS(cSess *session.ConnSession) {
193 | // dns
194 | if len(cSess.DNS) > 0 {
195 | // 使用动态域名路由时 DNS 一定走 VPN 才能进行流量分析
196 | if len(cSess.DynamicSplitIncludeDomains) > 0 {
197 | DynamicAddIncludeRoutes(cSess.DNS)
198 | }
199 |
200 | // 部分云服务器会在设置路由时重写 /etc/resolv.conf,延迟两秒再设置
201 | go func() {
202 | utils.CopyFile("/tmp/resolv.conf.bak", "/etc/resolv.conf")
203 |
204 | var dnsString string
205 | for _, dns := range cSess.DNS {
206 | dnsString += fmt.Sprintf("nameserver %s\n", dns)
207 | }
208 | time.Sleep(2 * time.Second)
209 | // OpenWrt 会将 127.0.0.1 写在最下面,影响其上面的解析
210 | err := utils.NewRecord("/etc/resolv.conf").Write(dnsString, false)
211 | if err != nil {
212 | base.Error("set DNS failed")
213 | }
214 | }()
215 | }
216 | }
217 |
218 | func restoreDNS(cSess *session.ConnSession) {
219 | // dns
220 | // 软件崩溃会导致无法恢复 resolv.conf 从而无法上网,需要重启系统
221 | if len(cSess.DNS) > 0 {
222 | utils.CopyFile("/etc/resolv.conf", "/tmp/resolv.conf.bak")
223 | }
224 | }
225 |
226 | func execCmd(cmdStrs []string) error {
227 | for _, cmdStr := range cmdStrs {
228 | cmd := exec.Command("sh", "-c", cmdStr)
229 | stdoutStderr, err := cmd.CombinedOutput()
230 | if err != nil {
231 | return fmt.Errorf("%s %s %s", err, cmd.String(), string(stdoutStderr))
232 | }
233 | }
234 | return nil
235 | }
236 |
--------------------------------------------------------------------------------
/utils/vpnc/vpnc_windows.go:
--------------------------------------------------------------------------------
1 | package vpnc
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "net/netip"
7 | "os/exec"
8 | "strings"
9 |
10 | "golang.org/x/sys/windows"
11 | "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
12 | "sslcon/base"
13 | "sslcon/session"
14 | "sslcon/tun"
15 | "sslcon/utils"
16 | )
17 |
18 | var (
19 | localInterface winipcfg.LUID
20 | iface winipcfg.LUID
21 | nextHopVPN netip.Addr
22 | nextHopGateway netip.Addr
23 | )
24 |
25 | func ConfigInterface(cSess *session.ConnSession) error {
26 | mtu, _ := tun.NativeTunDevice.MTU()
27 | err := SetMTU(cSess.TunName, mtu)
28 | if err != nil {
29 | return err
30 | }
31 |
32 | iface = tun.NativeTunDevice.LUID()
33 |
34 | // ip address
35 | iface.FlushIPAddresses(windows.AF_UNSPEC)
36 |
37 | nextHopVPN, _ = netip.ParseAddr(cSess.VPNAddress)
38 | prefixVPN, _ := netip.ParsePrefix(utils.IpMask2CIDR(cSess.VPNAddress, cSess.VPNMask))
39 | err = iface.SetIPAddressesForFamily(windows.AF_INET, []netip.Prefix{prefixVPN})
40 |
41 | return err
42 | }
43 |
44 | func SetRoutes(cSess *session.ConnSession) error {
45 | // routes
46 | dst, err := netip.ParsePrefix(cSess.ServerAddress + "/32")
47 | nextHopGateway, _ = netip.ParseAddr(base.LocalInterface.Gateway)
48 | err = localInterface.AddRoute(dst, nextHopGateway, 5)
49 | if err != nil {
50 | if !strings.HasSuffix(err.Error(), "exists.") {
51 | return routingError(dst, err)
52 | }
53 | }
54 |
55 | // Windows 排除路由 metric 相对大小好像不起作用,但不影响效果
56 | if len(cSess.SplitInclude) == 0 {
57 | cSess.SplitInclude = append(cSess.SplitInclude, "0.0.0.0/0.0.0.0")
58 | }
59 | for _, ipMask := range cSess.SplitInclude {
60 | dst, _ = netip.ParsePrefix(utils.IpMaskToCIDR(ipMask))
61 | err = iface.AddRoute(dst, nextHopVPN, 6)
62 | if err != nil {
63 | if !strings.HasSuffix(err.Error(), "exists.") {
64 | return routingError(dst, err)
65 | }
66 | }
67 | }
68 |
69 | if len(cSess.SplitExclude) > 0 {
70 | for _, ipMask := range cSess.SplitExclude {
71 | dst, _ = netip.ParsePrefix(utils.IpMaskToCIDR(ipMask))
72 | err = localInterface.AddRoute(dst, nextHopGateway, 5)
73 | if err != nil {
74 | if !strings.HasSuffix(err.Error(), "exists.") {
75 | return routingError(dst, err)
76 | }
77 | }
78 | }
79 | }
80 |
81 | // dns
82 | if len(cSess.DNS) > 0 {
83 | err = setDNS(cSess)
84 | }
85 | return err
86 | }
87 |
88 | func ResetRoutes(cSess *session.ConnSession) {
89 | dst, _ := netip.ParsePrefix(cSess.ServerAddress + "/32")
90 | localInterface.DeleteRoute(dst, nextHopGateway)
91 |
92 | if len(cSess.SplitExclude) > 0 {
93 | for _, ipMask := range cSess.SplitExclude {
94 | dst, _ = netip.ParsePrefix(utils.IpMaskToCIDR(ipMask))
95 | localInterface.DeleteRoute(dst, nextHopGateway)
96 | }
97 | }
98 |
99 | if len(cSess.DynamicSplitExcludeDomains) > 0 {
100 | cSess.DynamicSplitExcludeResolved.Range(func(_, value any) bool {
101 | ips := value.([]string)
102 | for _, ip := range ips {
103 | dst, _ = netip.ParsePrefix(ip + "/32")
104 | localInterface.DeleteRoute(dst, nextHopGateway)
105 | }
106 |
107 | return true
108 | })
109 | }
110 | }
111 |
112 | func DynamicAddIncludeRoutes(ips []string) {
113 | for _, ip := range ips {
114 | dst, _ := netip.ParsePrefix(ip + "/32")
115 | _ = iface.AddRoute(dst, nextHopVPN, 6)
116 | }
117 | }
118 |
119 | func DynamicAddExcludeRoutes(ips []string) {
120 | for _, ip := range ips {
121 | dst, _ := netip.ParsePrefix(ip + "/32")
122 | _ = localInterface.AddRoute(dst, nextHopGateway, 5)
123 | }
124 | }
125 |
126 | func GetLocalInterface() error {
127 | ifcs, err := winipcfg.GetAdaptersAddresses(windows.AF_INET, winipcfg.GAAFlagIncludeGateways)
128 | if err != nil {
129 | return err
130 | }
131 |
132 | var primaryInterface *winipcfg.IPAdapterAddresses
133 | var virtualPrimaryInterface *winipcfg.IPAdapterAddresses
134 | for _, ifc := range ifcs {
135 | base.Debug(ifc.AdapterName(), ifc.Description(), ifc.FriendlyName(), ifc.Ipv4Metric, ifc.IfType)
136 | // exclude Virtual Ethernet and Loopback Adapter
137 | // https://git.zx2c4.com/wireguard-windows/tree/tunnel/winipcfg/types.go?h=v0.5.3#n61
138 | if (ifc.IfType == 6 || ifc.IfType == 71) && ifc.FirstGatewayAddress != nil {
139 | if primaryInterface == nil || (ifc.Ipv4Metric < primaryInterface.Ipv4Metric) {
140 | if !strings.Contains(ifc.Description(), "Virtual") {
141 | primaryInterface = ifc
142 | } else if virtualPrimaryInterface == nil {
143 | virtualPrimaryInterface = ifc
144 | }
145 | }
146 | }
147 | }
148 |
149 | if primaryInterface == nil {
150 | if virtualPrimaryInterface != nil {
151 | primaryInterface = virtualPrimaryInterface
152 | } else {
153 | return fmt.Errorf("unable to find a valid network interface")
154 | }
155 | }
156 |
157 | base.Info("GetLocalInterface:", primaryInterface.AdapterName(), primaryInterface.Description(),
158 | primaryInterface.FriendlyName(), primaryInterface.Ipv4Metric, primaryInterface.IfType)
159 |
160 | base.LocalInterface.Name = primaryInterface.FriendlyName()
161 | base.LocalInterface.Ip4 = primaryInterface.FirstUnicastAddress.Address.IP().String()
162 | base.LocalInterface.Gateway = primaryInterface.FirstGatewayAddress.Address.IP().String()
163 | base.LocalInterface.Mac = net.HardwareAddr(primaryInterface.PhysicalAddress()).String()
164 |
165 | localInterface = primaryInterface.LUID
166 |
167 | return nil
168 | }
169 |
170 | func SetMTU(ifname string, mtu int) error {
171 | cmdStr := fmt.Sprintf("netsh interface ipv4 set subinterface \"%s\" MTU=%d", ifname, mtu)
172 | err := execCmd([]string{cmdStr})
173 | return err
174 | }
175 |
176 | func routingError(dst netip.Prefix, err error) error {
177 | return fmt.Errorf("routing error: %s %s", dst.String(), err)
178 | }
179 |
180 | func execCmd(cmdStrs []string) error {
181 | for _, cmdStr := range cmdStrs {
182 | cmd := exec.Command("cmd", "/C", cmdStr)
183 | stdoutStderr, err := cmd.CombinedOutput()
184 | if err != nil {
185 | return fmt.Errorf("%s %s %s", err, cmd.String(), string(stdoutStderr))
186 | }
187 | }
188 | return nil
189 | }
190 |
191 | func setDNS(cSess *session.ConnSession) error {
192 |
193 | if len(cSess.DynamicSplitIncludeDomains) > 0 {
194 | DynamicAddIncludeRoutes(cSess.DNS)
195 | }
196 |
197 | var servers []netip.Addr
198 | for _, dns := range cSess.DNS {
199 | addr, _ := netip.ParseAddr(dns)
200 | servers = append(servers, addr)
201 | }
202 |
203 | err := iface.SetDNS(windows.AF_INET, servers, []string{})
204 | return err
205 | }
206 |
--------------------------------------------------------------------------------
/utils/waterutil/ip_protocols.go:
--------------------------------------------------------------------------------
1 | package waterutil
2 |
3 | type IPProtocol byte
4 |
5 | // IP Protocols. From: http://en.wikipedia.org/wiki/List_of_IP_protocol_numbers
6 | const (
7 | HOPOPT = 0x00
8 | ICMP = 0x01
9 | IGMP = 0x02
10 | GGP = 0x03
11 | IPv4Encapsulation = 0x04
12 | ST = 0x05
13 | TCP = 0x06
14 | CBT = 0x07
15 | EGP = 0x08
16 | IGP = 0x09
17 | BBN_RCC_MON = 0x0A
18 | NVP_II = 0x0B
19 | PUP = 0x0C
20 | ARGUS = 0x0D
21 | EMCON = 0x0E
22 | XNET = 0x0F
23 | CHAOS = 0x10
24 | UDP = 0x11
25 | MUX = 0x12
26 | DCN_MEAS = 0x13
27 | HMP = 0x14
28 | PRM = 0x15
29 | XNS_IDP = 0x16
30 | TRUNK_1 = 0x17
31 | TRUNK_2 = 0x18
32 | LEAF_1 = 0x19
33 | LEAF_2 = 0x1A
34 | RDP = 0x1B
35 | IRTP = 0x1C
36 | ISO_TP4 = 0x1D
37 | NETBLT = 0x1E
38 | MFE_NSP = 0x1F
39 | MERIT_INP = 0x20
40 | DCCP = 0x21
41 | ThirdPC = 0x22
42 | IDPR = 0x23
43 | XTP = 0x24
44 | DDP = 0x25
45 | IDPR_CMTP = 0x26
46 | TPxx = 0x27
47 | IL = 0x28
48 | IPv6Encapsulation = 0x29
49 | SDRP = 0x2A
50 | IPv6_Route = 0x2B
51 | IPv6_Frag = 0x2C
52 | IDRP = 0x2D
53 | RSVP = 0x2E
54 | GRE = 0x2F
55 | MHRP = 0x30
56 | BNA = 0x31
57 | ESP = 0x32
58 | AH = 0x33
59 | I_NLSP = 0x34
60 | SWIPE = 0x35
61 | NARP = 0x36
62 | MOBILE = 0x37
63 | TLSP = 0x38
64 | SKIP = 0x39
65 | IPv6_ICMP = 0x3A
66 | IPv6_NoNxt = 0x3B
67 | IPv6_Opts = 0x3C
68 | CFTP = 0x3E
69 | SAT_EXPAK = 0x40
70 | KRYPTOLAN = 0x41
71 | RVD = 0x42
72 | IPPC = 0x43
73 | SAT_MON = 0x45
74 | VISA = 0x46
75 | IPCV = 0x47
76 | CPNX = 0x48
77 | CPHB = 0x49
78 | WSN = 0x4A
79 | PVP = 0x4B
80 | BR_SAT_MON = 0x4C
81 | SUN_ND = 0x4D
82 | WB_MON = 0x4E
83 | WB_EXPAK = 0x4F
84 | ISO_IP = 0x50
85 | VMTP = 0x51
86 | SECURE_VMTP = 0x52
87 | VINES = 0x53
88 | TTP = 0x54
89 | IPTM = 0x54
90 | NSFNET_IGP = 0x55
91 | DGP = 0x56
92 | TCF = 0x57
93 | EIGRP = 0x58
94 | OSPF = 0x59
95 | Sprite_RPC = 0x5A
96 | LARP = 0x5B
97 | MTP = 0x5C
98 | AX_25 = 0x5D
99 | IPIP = 0x5E
100 | MICP = 0x5F
101 | SCC_SP = 0x60
102 | ETHERIP = 0x61
103 | ENCAP = 0x62
104 | GMTP = 0x64
105 | IFMP = 0x65
106 | PNNI = 0x66
107 | PIM = 0x67
108 | ARIS = 0x68
109 | SCPS = 0x69
110 | QNX = 0x6A
111 | A_N = 0x6B
112 | IPComp = 0x6C
113 | SNP = 0x6D
114 | Compaq_Peer = 0x6E
115 | IPX_in_IP = 0x6F
116 | VRRP = 0x70
117 | PGM = 0x71
118 | L2TP = 0x73
119 | DDX = 0x74
120 | IATP = 0x75
121 | STP = 0x76
122 | SRP = 0x77
123 | UTI = 0x78
124 | SMP = 0x79
125 | SM = 0x7A
126 | PTP = 0x7B
127 | FIRE = 0x7D
128 | CRTP = 0x7E
129 | CRUDP = 0x7F
130 | SSCOPMCE = 0x80
131 | IPLT = 0x81
132 | SPS = 0x82
133 | PIPE = 0x83
134 | SCTP = 0x84
135 | FC = 0x85
136 | manet = 0x8A
137 | HIP = 0x8B
138 | Shim6 = 0x8C
139 | )
140 |
--------------------------------------------------------------------------------
/utils/waterutil/utils_ipv4.go:
--------------------------------------------------------------------------------
1 | package waterutil
2 |
3 | import (
4 | "net"
5 | )
6 |
7 | func IPv4DSCP(packet []byte) byte {
8 | return packet[1] >> 2
9 | }
10 |
11 | func IPv4ECN(packet []byte) byte {
12 | return packet[1] & 0x03
13 | }
14 |
15 | func IPv4Identification(packet []byte) [2]byte {
16 | return [2]byte{packet[4], packet[5]}
17 | }
18 |
19 | func IPv4TTL(packet []byte) byte {
20 | return packet[8]
21 | }
22 |
23 | func IPv4Protocol(packet []byte) IPProtocol {
24 | return IPProtocol(packet[9])
25 | }
26 |
27 | func IPv4Source(packet []byte) net.IP {
28 | return net.IPv4(packet[12], packet[13], packet[14], packet[15])
29 | }
30 |
31 | func SetIPv4Source(packet []byte, source net.IP) {
32 | copy(packet[12:16], source.To4())
33 | }
34 |
35 | func IPv4Destination(packet []byte) net.IP {
36 | return net.IPv4(packet[16], packet[17], packet[18], packet[19])
37 | }
38 |
39 | func SetIPv4Destination(packet []byte, dest net.IP) {
40 | copy(packet[16:20], dest.To4())
41 | }
42 |
43 | func IPv4Payload(packet []byte) []byte {
44 | ihl := packet[0] & 0x0F
45 | return packet[ihl*4:]
46 | }
47 |
48 | // For TCP/UDP
49 | func IPv4SourcePort(packet []byte) uint16 {
50 | payload := IPv4Payload(packet)
51 | return (uint16(payload[0]) << 8) | uint16(payload[1])
52 | }
53 |
54 | func IPv4DestinationPort(packet []byte) uint16 {
55 | payload := IPv4Payload(packet)
56 | return (uint16(payload[2]) << 8) | uint16(payload[3])
57 | }
58 |
59 | func SetIPv4SourcePort(packet []byte, port uint16) {
60 | payload := IPv4Payload(packet)
61 | payload[0] = byte(port >> 8)
62 | payload[1] = byte(port & 0xFF)
63 | }
64 |
65 | func SetIPv4DestinationPort(packet []byte, port uint16) {
66 | payload := IPv4Payload(packet)
67 | payload[2] = byte(port >> 8)
68 | payload[3] = byte(port & 0xFF)
69 | }
70 |
--------------------------------------------------------------------------------
/vpn/buffer.go:
--------------------------------------------------------------------------------
1 | package vpn
2 |
3 | import (
4 | "sync"
5 |
6 | "sslcon/proto"
7 | )
8 |
9 | const BufferSize = 2048
10 |
11 | // pool 实际数据缓冲区,缓冲区的容量由 golang 自动控制,PayloadIn 等通道只是个内存地址列表
12 | var pool = sync.Pool{
13 | New: func() interface{} {
14 | b := make([]byte, BufferSize)
15 | pl := proto.Payload{
16 | Type: 0x00,
17 | Data: b,
18 | }
19 | return &pl
20 | },
21 | }
22 |
23 | func getPayloadBuffer() *proto.Payload {
24 | pl := pool.Get().(*proto.Payload)
25 | return pl
26 | }
27 |
28 | func putPayloadBuffer(pl *proto.Payload) {
29 | // DPD-REQ、KEEPALIVE 等数据
30 | if cap(pl.Data) != BufferSize {
31 | // base.Debug("payload is:", pl.Data)
32 | return
33 | }
34 |
35 | pl.Type = 0x00
36 | pl.Data = pl.Data[:BufferSize]
37 | pool.Put(pl)
38 | }
39 |
--------------------------------------------------------------------------------
/vpn/dtls.go:
--------------------------------------------------------------------------------
1 | package vpn
2 |
3 | import (
4 | "context"
5 | "encoding/hex"
6 | "net"
7 | "strconv"
8 | "time"
9 |
10 | "github.com/pion/dtls/v3"
11 | "sslcon/base"
12 | "sslcon/proto"
13 | "sslcon/session"
14 | )
15 |
16 | // 新建 dtls.Conn
17 | func dtlsChannel(cSess *session.ConnSession) {
18 | var (
19 | conn *dtls.Conn
20 | dSess *session.DtlsSession
21 | err error
22 | bytesReceived int
23 | dead = time.Duration(cSess.DTLSDpdTime+5) * time.Second
24 | )
25 | defer func() {
26 | base.Info("dtls channel exit")
27 | if conn != nil {
28 | _ = conn.Close()
29 | }
30 | if dSess != nil {
31 | dSess.Close()
32 | }
33 | }()
34 |
35 | port, _ := strconv.Atoi(cSess.DTLSPort)
36 | addr := &net.UDPAddr{IP: net.ParseIP(cSess.ServerAddress), Port: port}
37 |
38 | id, _ := hex.DecodeString(cSess.DTLSId)
39 |
40 | config := &dtls.Config{
41 | InsecureSkipVerify: true,
42 | ExtendedMasterSecret: dtls.DisableExtendedMasterSecret,
43 | CipherSuites: func() []dtls.CipherSuiteID {
44 | switch cSess.DTLSCipherSuite {
45 | case "ECDHE-ECDSA-AES128-GCM-SHA256":
46 | return []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}
47 | case "ECDHE-RSA-AES128-GCM-SHA256":
48 | return []dtls.CipherSuiteID{dtls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}
49 | case "ECDHE-ECDSA-AES256-GCM-SHA384":
50 | return []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384}
51 | case "ECDHE-RSA-AES256-GCM-SHA384":
52 | return []dtls.CipherSuiteID{dtls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384}
53 | default:
54 | return []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}
55 | }
56 | }(),
57 | SessionStore: &SessionStore{dtls.Session{ID: id, Secret: session.Sess.PreMasterSecret}},
58 | // PSK: func(hint []byte) ([]byte, error) {
59 | // // return []byte{0xAB, 0xC1, 0x23}, nil
60 | // return id, nil
61 | // },
62 | // PSKIdentityHint: id,
63 | }
64 |
65 | conn, err = dtls.Dial("udp4", addr, config)
66 | // https://github.com/pion/dtls/pull/649
67 | if err != nil {
68 | base.Error(err)
69 | close(cSess.DtlsSetupChan) // 没有成功建立 DTLS 隧道
70 | return
71 | }
72 | ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
73 | defer cancel()
74 | if err = conn.HandshakeContext(ctx); err != nil {
75 | base.Error(err)
76 | close(cSess.DtlsSetupChan) // 没有成功建立 DTLS 隧道
77 | return
78 | }
79 |
80 | cSess.DtlsConnected.Store(true)
81 | dSess = cSess.DSess
82 | close(cSess.DtlsSetupChan) // 成功建立 DTLS 隧道
83 |
84 | // rewrite cSess.DTLSCipherSuite
85 | state, success := conn.ConnectionState()
86 | if success {
87 | cSess.DTLSCipherSuite = dtls.CipherSuiteName(state.CipherSuiteID)
88 | } else {
89 | cSess.DTLSCipherSuite = ""
90 | }
91 |
92 | base.Info("dtls channel negotiation succeeded")
93 |
94 | go payloadOutDTLSToServer(conn, dSess, cSess)
95 |
96 | // Step 21 serverToPayloadIn
97 | // 读取服务器返回的数据,调整格式,放入 cSess.PayloadIn,不再用子协程是为了能够退出 dtlsChannel 协程
98 | for {
99 | // 重置超时限制
100 | if cSess.ResetDTLSReadDead.Load() {
101 | _ = conn.SetReadDeadline(time.Now().Add(dead))
102 | cSess.ResetDTLSReadDead.Store(false)
103 | }
104 |
105 | pl := getPayloadBuffer() // 从池子申请一块内存,存放去除头部的数据包到 PayloadIn,在 payloadInToTun 中释放
106 | bytesReceived, err = conn.Read(pl.Data) // 服务器没有数据返回时,会阻塞
107 | if err != nil {
108 | base.Error("dtls server to payloadIn error:", err)
109 | return
110 | }
111 |
112 | // base.Debug("dtls server to payloadIn")
113 | // https://datatracker.ietf.org/doc/html/draft-mavrogiannopoulos-openconnect-02#section-2.3
114 | // UDP 数据包的头部只有 1 字节
115 | switch pl.Data[0] {
116 | case 0x07: // KEEPALIVE
117 | // base.Debug("dtls receive KEEPALIVE")
118 | case 0x05: // DISCONNECT
119 | // base.Debug("dtls receive DISCONNECT")
120 | return
121 | case 0x03: // DPD-REQ
122 | // base.Debug("dtls receive DPD-REQ")
123 | pl.Type = 0x04
124 | select {
125 | case cSess.PayloadOutDTLS <- pl:
126 | case <-dSess.CloseChan:
127 | }
128 | case 0x04:
129 | base.Debug("dtls receive DPD-RESP")
130 | case 0x00: // DATA
131 | pl.Data = append(pl.Data[:0], pl.Data[1:bytesReceived]...)
132 | select {
133 | case cSess.PayloadIn <- pl:
134 | case <-dSess.CloseChan:
135 | return
136 | }
137 | }
138 | cSess.Stat.BytesReceived += uint64(bytesReceived)
139 | }
140 | }
141 |
142 | // payloadOutDTLSToServer Step 4
143 | func payloadOutDTLSToServer(conn *dtls.Conn, dSess *session.DtlsSession, cSess *session.ConnSession) {
144 | defer func() {
145 | base.Info("dtls payloadOut to server exit")
146 | _ = conn.Close()
147 | dSess.Close()
148 | }()
149 |
150 | var (
151 | err error
152 | bytesSent int
153 | pl *proto.Payload
154 | )
155 |
156 | for {
157 | select {
158 | case pl = <-cSess.PayloadOutDTLS:
159 | case <-dSess.CloseChan:
160 | return
161 | }
162 |
163 | // base.Debug("dtls payloadOut to server")
164 | if pl.Type == 0x00 {
165 | // 获取数据长度
166 | l := len(pl.Data)
167 | // 先扩容 +1
168 | pl.Data = pl.Data[:l+1]
169 | // 数据后移
170 | copy(pl.Data[1:], pl.Data)
171 | // 添加头信息
172 | pl.Data[0] = pl.Type
173 | } else {
174 | // 设置头类型
175 | pl.Data = append(pl.Data[:0], pl.Type)
176 | }
177 |
178 | bytesSent, err = conn.Write(pl.Data)
179 | if err != nil {
180 | base.Error("dtls payloadOut to server error:", err)
181 | return
182 | }
183 | cSess.Stat.BytesSent += uint64(bytesSent)
184 |
185 | // 释放由 tunToPayloadOut 申请的内存
186 | putPayloadBuffer(pl)
187 | }
188 | }
189 |
190 | type SessionStore struct {
191 | sess dtls.Session
192 | }
193 |
194 | func (store *SessionStore) Set([]byte, dtls.Session) error {
195 | return nil
196 | }
197 |
198 | func (store *SessionStore) Get([]byte) (dtls.Session, error) {
199 | return store.sess, nil
200 | }
201 |
202 | func (store *SessionStore) Del([]byte) error {
203 | return nil
204 | }
205 |
--------------------------------------------------------------------------------
/vpn/tls.go:
--------------------------------------------------------------------------------
1 | package vpn
2 |
3 | import (
4 | "bufio"
5 | "crypto/tls"
6 | "encoding/binary"
7 | "net/http"
8 | "time"
9 |
10 | "sslcon/base"
11 | "sslcon/proto"
12 | "sslcon/session"
13 | )
14 |
15 | // 复用已有的 tls.Conn 和对应的 bufR
16 | func tlsChannel(conn *tls.Conn, bufR *bufio.Reader, cSess *session.ConnSession, resp *http.Response) {
17 | defer func() {
18 | base.Info("tls channel exit")
19 | resp.Body.Close()
20 | _ = conn.Close()
21 | cSess.Close()
22 | }()
23 | var (
24 | err error
25 | bytesReceived int
26 | dataLen uint16
27 | dead = time.Duration(cSess.TLSDpdTime+5) * time.Second
28 | )
29 |
30 | go payloadOutTLSToServer(conn, cSess)
31 |
32 | // Step 21 serverToPayloadIn
33 | // 读取服务器返回的数据,调整格式,放入 cSess.PayloadIn
34 | for {
35 | // 重置超时限制
36 | if cSess.ResetTLSReadDead.Load() {
37 | _ = conn.SetReadDeadline(time.Now().Add(dead))
38 | cSess.ResetTLSReadDead.Store(false)
39 | }
40 |
41 | pl := getPayloadBuffer() // 从池子申请一块内存,存放去除头部的数据包到 PayloadIn,在 payloadInToTun 中释放
42 | bytesReceived, err = bufR.Read(pl.Data) // 服务器没有数据返回时,会阻塞
43 | if err != nil {
44 | base.Error("tls server to payloadIn error:", err)
45 | return
46 | }
47 |
48 | // base.Debug("tls server to payloadIn", "Type", pl.Data[6])
49 | // https://datatracker.ietf.org/doc/html/draft-mavrogiannopoulos-openconnect-03#section-2.2
50 | switch pl.Data[6] {
51 | case 0x00: // DATA
52 | // base.Debug("tls receive DATA")
53 | // 获取数据长度
54 | dataLen = binary.BigEndian.Uint16(pl.Data[4:6])
55 | // 去除数据头
56 | copy(pl.Data, pl.Data[8:8+dataLen])
57 | // 更新切片长度
58 | pl.Data = pl.Data[:dataLen]
59 |
60 | select {
61 | case cSess.PayloadIn <- pl:
62 | case <-cSess.CloseChan:
63 | return
64 | }
65 | case 0x04:
66 | base.Debug("tls receive DPD-RESP")
67 | case 0x03: // DPD-REQ
68 | pl.Type = 0x04
69 | select {
70 | case cSess.PayloadOutTLS <- pl:
71 | case <-cSess.CloseChan:
72 | return
73 | }
74 | }
75 | cSess.Stat.BytesReceived += uint64(bytesReceived)
76 | }
77 | }
78 |
79 | // payloadOutTLSToServer Step 4
80 | func payloadOutTLSToServer(conn *tls.Conn, cSess *session.ConnSession) {
81 | defer func() {
82 | base.Info("tls payloadOut to server exit")
83 | _ = conn.Close()
84 | cSess.Close()
85 | }()
86 |
87 | var (
88 | err error
89 | bytesSent int
90 | pl *proto.Payload
91 | )
92 |
93 | for {
94 | select {
95 | case pl = <-cSess.PayloadOutTLS:
96 | case <-cSess.CloseChan:
97 | return
98 | }
99 |
100 | // base.Debug("tls payloadOut to server", "Type", pl.Type)
101 | if pl.Type == 0x00 {
102 | // 获取数据长度
103 | l := len(pl.Data)
104 | // 先扩容 +8
105 | pl.Data = pl.Data[:l+8]
106 | // 数据后移
107 | copy(pl.Data[8:], pl.Data)
108 | // 添加头信息
109 | copy(pl.Data[:8], proto.Header)
110 | // 更新头长度
111 | binary.BigEndian.PutUint16(pl.Data[4:6], uint16(l))
112 | } else {
113 | pl.Data = append(pl.Data[:0], proto.Header...)
114 | // 设置头类型
115 | pl.Data[6] = pl.Type
116 | }
117 | bytesSent, err = conn.Write(pl.Data)
118 | if err != nil {
119 | base.Error("tls payloadOut to server error:", err)
120 | return
121 | }
122 | cSess.Stat.BytesSent += uint64(bytesSent)
123 |
124 | // 释放由 tunToPayloadOut 申请的内存
125 | putPayloadBuffer(pl)
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/vpn/tun.go:
--------------------------------------------------------------------------------
1 | package vpn
2 |
3 | import (
4 | "runtime"
5 |
6 | "github.com/gopacket/gopacket"
7 | "github.com/gopacket/gopacket/layers"
8 | "sslcon/base"
9 | "sslcon/proto"
10 | "sslcon/session"
11 | "sslcon/tun"
12 | "sslcon/utils"
13 | "sslcon/utils/vpnc"
14 | )
15 |
16 | var offset = 0 // reserve space for header
17 |
18 | func setupTun(cSess *session.ConnSession) error {
19 | if runtime.GOOS == "windows" {
20 | cSess.TunName = "SSLCon"
21 | } else if runtime.GOOS == "darwin" {
22 | cSess.TunName = "utun"
23 | offset = 4
24 | } else {
25 | cSess.TunName = "sslcon"
26 | }
27 | dev, err := tun.CreateTUN(cSess.TunName, cSess.MTU)
28 | if err != nil {
29 | base.Error("failed to creates a new tun interface")
30 | return err
31 | }
32 | if runtime.GOOS == "darwin" {
33 | cSess.TunName, _ = dev.Name()
34 | }
35 |
36 | base.Debug("tun device:", cSess.TunName)
37 | tun.NativeTunDevice = dev.(*tun.NativeTun)
38 |
39 | // 不可并行
40 | err = vpnc.ConfigInterface(cSess)
41 | if err != nil {
42 | _ = dev.Close()
43 | return err
44 | }
45 |
46 | go tunToPayloadOut(dev, cSess) // read from apps
47 | go payloadInToTun(dev, cSess) // write to apps
48 | return nil
49 | }
50 |
51 | // Step 3
52 | // 网络栈将应用数据包转给 tun 后,该函数从 tun 读取数据包,放入 cSess.PayloadOutTLS 或 cSess.PayloadOutDTLS
53 | // 之后由 payloadOutTLSToServer 或 payloadOutDTLSToServer 调整格式,发送给服务端
54 | func tunToPayloadOut(dev tun.Device, cSess *session.ConnSession) {
55 | // tun 设备读错误
56 | defer func() {
57 | base.Info("tun to payloadOut exit")
58 | _ = dev.Close()
59 | }()
60 | var (
61 | err error
62 | n int
63 | )
64 |
65 | for {
66 | // 从池子申请一块内存,存放到 PayloadOutTLS 或 PayloadOutDTLS,在 payloadOutTLSToServer 或 payloadOutDTLSToServer 中释放
67 | // 由 payloadOutTLSToServer 或 payloadOutDTLSToServer 添加 header 后发送出去
68 | pl := getPayloadBuffer()
69 | n, err = dev.Read(pl.Data, offset) // 如果 tun 没有 up,会在这等待
70 | if err != nil {
71 | base.Error("tun to payloadOut error:", err)
72 | return
73 | }
74 |
75 | // 更新数据长度
76 | pl.Data = (pl.Data)[offset : offset+n]
77 |
78 | // base.Debug("tunToPayloadOut")
79 | // if base.Cfg.LogLevel == "Debug" {
80 | // src, srcPort, dst, dstPort := utils.ResolvePacket(pl.Data)
81 | // if dst == "8.8.8.8" {
82 | // base.Debug("client from", src, srcPort, "request target", dst, dstPort)
83 | // }
84 | // }
85 |
86 | dSess := cSess.DSess
87 | if cSess.DtlsConnected.Load() {
88 | select {
89 | case cSess.PayloadOutDTLS <- pl:
90 | case <-dSess.CloseChan:
91 | }
92 | } else {
93 | select {
94 | case cSess.PayloadOutTLS <- pl:
95 | case <-cSess.CloseChan:
96 | return
97 | }
98 | }
99 | }
100 | }
101 |
102 | // Step 22
103 | // 读取 tlsChannel、dtlsChannel 放入 cSess.PayloadIn 的数据包(由服务端返回,已调整格式),写入 tun,网络栈交给应用
104 | func payloadInToTun(dev tun.Device, cSess *session.ConnSession) {
105 | // tun 设备写错误或者cSess.CloseChan
106 | defer func() {
107 | base.Info("payloadIn to tun exit")
108 | if !cSess.Sess.ActiveClose {
109 | vpnc.ResetRoutes(cSess) // 如果 tun 没有创建成功,也不会调用 SetRoutes
110 | }
111 | // 可能由写错误触发,和 tunToPayloadOut 一起,只要有一处确保退出 cSess 即可,否则 tls 不会退出
112 | // 如果由外部触发,cSess.Close() 因为使用 sync.Once,所以没影响
113 | cSess.Close()
114 | _ = dev.Close()
115 | }()
116 |
117 | var (
118 | err error
119 | pl *proto.Payload
120 | )
121 |
122 | for {
123 | select {
124 | case pl = <-cSess.PayloadIn:
125 | case <-cSess.CloseChan:
126 | return
127 | }
128 |
129 | // 只有当使用域名分流且返回数据包为 DNS 时才进一步分析,少建几个协程
130 | if cSess.DynamicSplitTunneling {
131 | _, srcPort, _, _ := utils.ResolvePacket(pl.Data)
132 | if srcPort == 53 {
133 | go dynamicSplitRoutes(pl.Data, cSess)
134 | }
135 | }
136 | // base.Debug("payloadInToTun")
137 | // if base.Cfg.LogLevel == "Debug" {
138 | // src, srcPort, dst, dstPort := utils.ResolvePacket(pl.Data)
139 | // if src == "8.8.8.8" {
140 | // base.Debug("target from", src, srcPort, "response to client", dst, dstPort)
141 | // }
142 | // }
143 |
144 | if offset > 0 {
145 | expand := make([]byte, offset+len(pl.Data))
146 | copy(expand[offset:], pl.Data)
147 | _, err = dev.Write(expand, offset)
148 | } else {
149 | _, err = dev.Write(pl.Data, offset)
150 | }
151 |
152 | if err != nil {
153 | base.Error("payloadIn to tun error:", err)
154 | return
155 | }
156 |
157 | // 释放由 serverToPayloadIn 申请的内存
158 | putPayloadBuffer(pl)
159 | }
160 | }
161 |
162 | func dynamicSplitRoutes(data []byte, cSess *session.ConnSession) {
163 | packet := gopacket.NewPacket(data, layers.LayerTypeIPv4, gopacket.Default)
164 | dnsLayer := packet.Layer(layers.LayerTypeDNS)
165 | if dnsLayer != nil {
166 | dns, _ := dnsLayer.(*layers.DNS)
167 |
168 | query := string(dns.Questions[0].Name)
169 | // base.Debug("Query:", query)
170 |
171 | if utils.InArrayGeneric(cSess.DynamicSplitIncludeDomains, query) {
172 | // 分析流量后才知道请求的域名,即使已经设置路由,仍然需要分析流量,不可避免的 overhead
173 | if _, ok := cSess.DynamicSplitIncludeResolved.Load(query); !ok && dns.ANCount > 0 {
174 | var answers []string
175 | for _, v := range dns.Answers {
176 | // log.Printf("DNS Answer: %+v", v)
177 | if v.Type == layers.DNSTypeA {
178 | // fmt.Println("Name:", string(v.Name)) // cname, canonical name
179 | // base.Debug("Address:", v.IP.String())
180 | answers = append(answers, v.IP.String())
181 | }
182 | }
183 | if len(answers) > 0 {
184 | cSess.DynamicSplitIncludeResolved.Store(query, answers)
185 | vpnc.DynamicAddIncludeRoutes(answers)
186 | }
187 | }
188 | } else if utils.InArrayGeneric(cSess.DynamicSplitExcludeDomains, query) {
189 | if _, ok := cSess.DynamicSplitExcludeResolved.Load(query); !ok && dns.ANCount > 0 {
190 | var answers []string
191 | for _, v := range dns.Answers {
192 | // log.Printf("DNS Answer: %+v", v)
193 | if v.Type == layers.DNSTypeA {
194 | // fmt.Println("Name:", string(v.Name)) // cname, canonical name
195 | // base.Debug("Address:", v.IP.String())
196 | answers = append(answers, v.IP.String())
197 | }
198 | }
199 | if len(answers) > 0 {
200 | cSess.DynamicSplitExcludeResolved.Store(query, answers)
201 | vpnc.DynamicAddExcludeRoutes(answers)
202 | }
203 | }
204 | }
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/vpn/tunnel.go:
--------------------------------------------------------------------------------
1 | package vpn
2 |
3 | import (
4 | "bytes"
5 | "crypto/tls"
6 | "encoding/hex"
7 | "fmt"
8 | "net/http"
9 | "strings"
10 |
11 | "sslcon/auth"
12 | "sslcon/base"
13 | "sslcon/session"
14 | "sslcon/utils"
15 | "sslcon/utils/vpnc"
16 | )
17 |
18 | var (
19 | reqHeaders = make(map[string]string)
20 | )
21 |
22 | func init() {
23 | reqHeaders["X-CSTP-VPNAddress-Type"] = "IPv4"
24 | // Payload + 8 + 加密扩展位 + TCP或UDP头 + IP头 最好小于 1500,这里参考 AnyConnect 设置
25 | reqHeaders["X-CSTP-MTU"] = "1399"
26 | reqHeaders["X-CSTP-Base-MTU"] = "1399"
27 | // if base.Cfg.OS == "android" || base.Cfg.OS == "ios" {
28 | // reqHeaders["X-CSTP-License"] = "mobile"
29 | // }
30 | }
31 |
32 | func initTunnel() {
33 | // https://datatracker.ietf.org/doc/html/draft-mavrogiannopoulos-openconnect-03#section-2.1.3
34 | reqHeaders["Cookie"] = "webvpn=" + session.Sess.SessionToken // 无论什么服务端都需要通过 Cookie 发送 Session
35 | reqHeaders["X-CSTP-Local-VPNAddress-IP4"] = base.LocalInterface.Ip4
36 |
37 | // Legacy Establishment of Secondary UDP Channel https://datatracker.ietf.org/doc/html/draft-mavrogiannopoulos-openconnect-02#section-2.1.5.1
38 | // worker-vpn.c WSPCONFIG(ws)->udp_port != 0 && req->master_secret_set != 0 否则 disabling UDP (DTLS) connection
39 | // 如果开启 dtls_psk(默认开启,见配置说明) 且 CipherSuite 包含 PSK-NEGOTIATE(仅限ocserv),worker-http.c 自动设置 req->master_secret_set = 1
40 | // 此时无需手动设置 Secret,会自动协商建立 dtls 链接,AnyConnect 客户端不支持
41 | session.Sess.PreMasterSecret, _ = utils.MakeMasterSecret()
42 | reqHeaders["X-DTLS-Master-Secret"] = hex.EncodeToString(session.Sess.PreMasterSecret) // A hex encoded pre-master secret to be used in the legacy DTLS session negotiation
43 |
44 | // https://gitlab.com/openconnect/ocserv/-/blob/master/src/worker-http.c#L150
45 | // https://github.com/openconnect/openconnect/blob/master/gnutls-dtls.c#L75
46 | reqHeaders["X-DTLS12-CipherSuite"] = "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:AES128-GCM-SHA256"
47 | }
48 |
49 | // SetupTunnel initiates an HTTP CONNECT command to establish a VPN
50 | func SetupTunnel() error {
51 | initTunnel()
52 |
53 | // https://github.com/golang/go/commit/da6c168378b4c1deb2a731356f1f438e4723b8a7
54 | // https://github.com/golang/go/issues/17227#issuecomment-341855744
55 | req, _ := http.NewRequest("CONNECT", auth.Prof.Scheme+auth.Prof.HostWithPort+"/CSCOSSLC/tunnel", nil)
56 | utils.SetCommonHeader(req)
57 | for k, v := range reqHeaders {
58 | // req.Header.Set 会将首字母大写,其它小写
59 | req.Header[k] = []string{v}
60 | }
61 |
62 | // 发送 CONNECT 请求
63 | err := req.Write(auth.Conn)
64 | if err != nil {
65 | auth.Conn.Close()
66 | return err
67 | }
68 | var resp *http.Response
69 | // resp.Body closed when tlsChannel exit
70 | resp, err = http.ReadResponse(auth.BufR, req)
71 | if err != nil {
72 | auth.Conn.Close()
73 | return err
74 | }
75 |
76 | if resp.StatusCode != http.StatusOK {
77 | auth.Conn.Close()
78 | return fmt.Errorf("tunnel negotiation failed %s", resp.Status)
79 | }
80 | // 协商成功,读取服务端返回的配置
81 | // https://datatracker.ietf.org/doc/html/draft-mavrogiannopoulos-openconnect-03#section-2.1.3
82 |
83 | // 提前判断是否调试模式,避免不必要的转换,http.ReadResponse.Header 将首字母大写,其余小写,即使服务端调试时正常
84 | if base.Cfg.LogLevel == "Debug" {
85 | headers := make([]byte, 0)
86 | buf := bytes.NewBuffer(headers)
87 | // http.ReadResponse: Keys in the map are canonicalized (see CanonicalHeaderKey).
88 | // https://ron-liu.medium.com/what-canonical-http-header-mean-in-golang-2e97f854316d
89 | _ = resp.Header.Write(buf)
90 | base.Debug(buf.String())
91 | }
92 |
93 | cSess := session.Sess.NewConnSession(&resp.Header)
94 | cSess.ServerAddress = strings.Split(auth.Conn.RemoteAddr().String(), ":")[0]
95 | cSess.Hostname = auth.Prof.Host
96 | cSess.TLSCipherSuite = tls.CipherSuiteName(auth.Conn.ConnectionState().CipherSuite)
97 |
98 | err = setupTun(cSess)
99 | if err != nil {
100 | auth.Conn.Close()
101 | cSess.Close()
102 | return err
103 | }
104 |
105 | // 为了靠谱,不再异步设置,路由多的话可能要等等
106 | err = vpnc.SetRoutes(cSess)
107 | if err != nil {
108 | auth.Conn.Close()
109 | cSess.Close()
110 | }
111 | base.Info("tls channel negotiation succeeded")
112 |
113 | // 只有网卡和路由设置成功才会进行下一步
114 | // https://datatracker.ietf.org/doc/html/draft-mavrogiannopoulos-openconnect-03#section-2.1.4
115 | go tlsChannel(auth.Conn, auth.BufR, cSess, resp)
116 |
117 | if !base.Cfg.NoDTLS && cSess.DTLSPort != "" {
118 | // https://datatracker.ietf.org/doc/html/draft-mavrogiannopoulos-openconnect-03#section-2.1.5
119 | go dtlsChannel(cSess)
120 | }
121 |
122 | cSess.DPDTimer()
123 | cSess.ReadDeadTimer()
124 |
125 | return err
126 | }
127 |
--------------------------------------------------------------------------------
/vpnagent.go:
--------------------------------------------------------------------------------
1 | //go:build linux || darwin || windows
2 |
3 | package main
4 |
5 | import (
6 | "fmt"
7 | "os"
8 | "os/signal"
9 | "syscall"
10 |
11 | "github.com/kardianos/service"
12 | "sslcon/base"
13 | "sslcon/rpc"
14 | "sslcon/svc"
15 | )
16 |
17 | func main() {
18 | // fmt.Println("os.Args: ", len(os.Args))
19 | if len(os.Args) < 2 {
20 | if service.Interactive() {
21 | base.Setup()
22 | rpc.Setup()
23 | watchSignal() // 主协程退出则应用退出
24 | } else {
25 | svc.RunSvc()
26 | }
27 | } else {
28 | cmd := os.Args[1]
29 | switch cmd {
30 | case "install":
31 | svc.InstallSvc()
32 | case "uninstall":
33 | svc.UninstallSvc()
34 | // todo uninstall wintun driver
35 | default:
36 | fmt.Println("invalid command: ", cmd)
37 | }
38 | }
39 | }
40 |
41 | func watchSignal() {
42 | base.Info("Server pid: ", os.Getpid())
43 |
44 | sigs := make(chan os.Signal, 1)
45 | // https://pkg.go.dev/os/signal
46 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
47 | for {
48 | // 没有信号就阻塞,从而避免主协程退出
49 | sig := <-sigs
50 | base.Info("Get signal:", sig)
51 | switch sig {
52 | default:
53 | base.Info("Stop")
54 | rpc.DisConnect()
55 | return
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------