├── .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 | 11 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | --------------------------------------------------------------------------------