├── models ├── deviceupdate.go ├── net.go ├── apierror.go └── register.go ├── main.go ├── cmd ├── version.go ├── root.go └── proxy.go ├── entrypoint.sh ├── .gitignore ├── internal ├── consts.go └── utils.go ├── Dockerfile ├── LICENSE ├── healthcheck.sh ├── go.mod ├── .github └── workflows │ ├── dev-docker.yml │ ├── docker-image.yml │ └── build-binaries.yml ├── api ├── resolver.go ├── masque.go ├── cloudflare.go └── tunnel.go ├── README.md ├── go.sum └── config └── config.go /models/deviceupdate.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type DeviceUpdate struct { 4 | Key string `json:"key"` 5 | KeyType string `json:"key_type"` 6 | TunType string `json:"tunnel_type"` 7 | Name string `json:"name,omitempty"` 8 | } 9 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/HynoR/uscf/cmd" 8 | ) 9 | 10 | func main() { 11 | if err := cmd.Execute(); err != nil { 12 | fmt.Println("Error:", err) 13 | os.Exit(1) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var ( 10 | version = "dev" 11 | commit = "none" 12 | date = "unknown" 13 | ) 14 | 15 | var versionCmd = &cobra.Command{ 16 | Use: "version", 17 | Short: "Print the version number of usque", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | fmt.Printf("usque version: %s\n", version) 20 | fmt.Printf("Commit: %s\n", commit) 21 | fmt.Printf("Build Date: %s\n", date) 22 | }, 23 | } 24 | 25 | func init() { 26 | rootCmd.AddCommand(versionCmd) 27 | } 28 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # Configuration file path 5 | CONFIG_FILE="/app/etc/config.json" 6 | 7 | echo "=========================" 8 | echo "Starting USCF proxy service..." 9 | echo "Configuration file path: $CONFIG_FILE" 10 | echo "=========================" 11 | 12 | # Catch SIGTERM and SIGINT signals for graceful exit 13 | trap "echo \"Received termination signal, shutting down service...\"; exit 0" TERM INT 14 | 15 | # Execute uscf proxy command, using the fixed configuration file path, and pass all additional command-line arguments 16 | exec /bin/uscf proxy -c "$CONFIG_FILE" "$@" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | -------------------------------------------------------------------------------- /models/net.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | // 超时管理的连接包装器 9 | type TimeoutConn struct { 10 | net.Conn 11 | IdleTimeout time.Duration 12 | } 13 | 14 | func (c *TimeoutConn) Read(b []byte) (int, error) { 15 | if c.IdleTimeout > 0 { 16 | err := c.Conn.SetReadDeadline(time.Now().Add(c.IdleTimeout)) 17 | if err != nil { 18 | return 0, err 19 | } 20 | } 21 | return c.Conn.Read(b) 22 | } 23 | 24 | func (c *TimeoutConn) Write(b []byte) (int, error) { 25 | if c.IdleTimeout > 0 { 26 | err := c.Conn.SetWriteDeadline(time.Now().Add(c.IdleTimeout)) 27 | if err != nil { 28 | return 0, err 29 | } 30 | } 31 | return c.Conn.Write(b) 32 | } 33 | -------------------------------------------------------------------------------- /internal/consts.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | const ( 4 | ApiUrl = "https://api.cloudflareclient.com" 5 | ApiVersion = "v0a4471" 6 | ConnectSNI = "consumer-masque.cloudflareclient.com" 7 | // unused for now 8 | ZeroTierSNI = "zt-masque.cloudflareclient.com" 9 | ConnectURI = "https://cloudflareaccess.com" 10 | DefaultModel = "PC" 11 | KeyTypeWg = "curve25519" 12 | TunTypeWg = "wireguard" 13 | KeyTypeMasque = "secp256r1" 14 | TunTypeMasque = "masque" 15 | DefaultLocale = "en_US" 16 | ) 17 | 18 | var Headers = map[string]string{ 19 | "User-Agent": "WARP for Android", 20 | "CF-Client-Version": "a-6.35-4471", 21 | "Content-Type": "application/json; charset=UTF-8", 22 | "Connection": "Keep-Alive", 23 | } 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod . 6 | COPY go.sum . 7 | 8 | RUN go mod download 9 | 10 | COPY . . 11 | 12 | RUN go build -o uscf -ldflags="-s -w" . 13 | 14 | # scratch won't be enough, because we need a cert store 15 | FROM alpine:latest 16 | 17 | WORKDIR /app 18 | 19 | # Create etc directory for configuration and install required tools 20 | RUN mkdir -p /app/etc && \ 21 | apk add --no-cache curl jq 22 | 23 | COPY --from=builder /app/uscf /bin/uscf 24 | # Copy the scripts from the build context 25 | COPY entrypoint.sh /app/entrypoint.sh 26 | COPY healthcheck.sh /app/healthcheck.sh 27 | RUN chmod +x /app/entrypoint.sh /app/healthcheck.sh 28 | 29 | # Add healthcheck 30 | HEALTHCHECK --interval=150s --timeout=10s --start-period=30s --retries=3 \ 31 | CMD /app/healthcheck.sh 32 | 33 | ENTRYPOINT ["/app/entrypoint.sh"] -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/HynoR/uscf/config" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var rootCmd = &cobra.Command{ 11 | Use: "usque", 12 | Short: "Usque Warp CLI", 13 | Long: "An unofficial Cloudflare Warp CLI that uses the MASQUE protocol and exposes the tunnel as various different services.", 14 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 15 | configPath, err := cmd.Flags().GetString("config") 16 | if err != nil { 17 | log.Fatalf("Failed to get config path: %v", err) 18 | } 19 | 20 | if configPath != "" { 21 | if err := config.LoadConfig(configPath); err != nil { 22 | log.Printf("Config file not found: %v", err) 23 | log.Printf("You may only use the register command to generate one.") 24 | } 25 | } 26 | }, 27 | } 28 | 29 | func Execute() error { 30 | return rootCmd.Execute() 31 | } 32 | 33 | func init() { 34 | rootCmd.PersistentFlags().StringP("config", "c", "config.json", "config file (default is config.json)") 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 KOMATA 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 | -------------------------------------------------------------------------------- /healthcheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 读取 SOCKS 配置 4 | CONFIG_PATH="/app/etc/config.json" 5 | PORT=$(jq -r '.socks.port' $CONFIG_PATH) 6 | USERNAME=$(jq -r '.socks.username' $CONFIG_PATH) 7 | PASSWORD=$(jq -r '.socks.password' $CONFIG_PATH) 8 | 9 | # 设置默认端口 10 | if [ -z "$PORT" ] || [ "$PORT" = "null" ]; then 11 | PORT="1080" # 默认端口 12 | fi 13 | 14 | # 使用 curl 通过 SOCKS 代理检查连接 15 | check_url() { 16 | if [ -n "$USERNAME" ] && [ "$USERNAME" != "null" ] && [ -n "$PASSWORD" ] && [ "$PASSWORD" != "null" ]; then 17 | curl --silent --connect-timeout 5 --max-time 10 --socks5 "127.0.0.1:$PORT" --proxy-user "$USERNAME:$PASSWORD" "$1" -o /dev/null -w "%{http_code}" 18 | else 19 | curl --silent --connect-timeout 5 --max-time 10 --socks5 "127.0.0.1:$PORT" "$1" -o /dev/null -w "%{http_code}" 20 | fi 21 | } 22 | 23 | status_gstatic=$(check_url "http://connectivitycheck.gstatic.com/generate_204") 24 | 25 | 26 | if [ "$status_gstatic" = "204" ]; then 27 | echo "[Health Check] OK(Google)" 28 | exit 0 29 | fi 30 | 31 | status_cloudflare=$(check_url "http://cp.cloudflare.com/") 32 | if [ "$status_cloudflare" = "204" ]; then 33 | echo "[Health Check] OK(Cloudflare)" 34 | exit 0 35 | else 36 | echo "[Health Check] Failed!!!" 37 | exit 1 38 | fi 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/HynoR/uscf 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/Diniboy1123/connect-ip-go v0.0.0-20250220050656-56698ca53ed4 7 | github.com/quic-go/quic-go v0.51.0 8 | github.com/spf13/cobra v1.9.1 9 | github.com/things-go/go-socks5 v0.0.6 10 | github.com/yosida95/uritemplate/v3 v3.0.2 11 | golang.zx2c4.com/wireguard v0.0.0-20250505131008-436f7fdc1670 12 | ) 13 | 14 | require ( 15 | github.com/dunglas/httpsfv v1.0.2 // indirect 16 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 17 | github.com/google/btree v1.1.2 // indirect 18 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect 19 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 20 | github.com/onsi/ginkgo/v2 v2.9.5 // indirect 21 | github.com/quic-go/qpack v0.5.1 // indirect 22 | github.com/spf13/pflag v1.0.6 // indirect 23 | go.uber.org/mock v0.5.0 // indirect 24 | golang.org/x/crypto v0.37.0 // indirect 25 | golang.org/x/mod v0.21.0 // indirect 26 | golang.org/x/net v0.39.0 // indirect 27 | golang.org/x/sync v0.13.0 // indirect 28 | golang.org/x/sys v0.32.0 // indirect 29 | golang.org/x/text v0.24.0 // indirect 30 | golang.org/x/time v0.7.0 // indirect 31 | golang.org/x/tools v0.26.0 // indirect 32 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect 33 | gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /models/apierror.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Known error messages from the API 4 | const ( 5 | InvalidPublicKey = "Invalid public key" 6 | ) 7 | 8 | type APIError struct { 9 | // not sure what type this is, so we will settle for interface{} 10 | // for now 11 | Result interface{} `json:"result"` 12 | Success bool `json:"success"` 13 | Errors []ErrorInfo `json:"errors"` 14 | Messages []string `json:"messages"` 15 | } 16 | 17 | type ErrorInfo struct { 18 | Code int `json:"code"` 19 | Message string `json:"message"` 20 | } 21 | 22 | // ErrorsAsString returns a string representation of the errors in the APIError. 23 | // It concatenates the error messages into a single string, separated by semicolons. 24 | // 25 | // Parameters: 26 | // - separator: string - The string to use as a separator between error messages. 27 | // 28 | // Returns: 29 | // - string: A string containing all error messages, separated by the specified separator. 30 | func (e *APIError) ErrorsAsString(separator string) string { 31 | var result string 32 | for _, err := range e.Errors { 33 | result += err.Message + separator 34 | } 35 | if len(result) > 0 { 36 | return result[:len(result)-len(separator)] 37 | } 38 | return result 39 | } 40 | 41 | // HasErrorMessage checks if the APIError contains a specific error message. 42 | // It returns true if the error message is found, otherwise false. 43 | // 44 | // Parameters: 45 | // - message: string - The error message to check for. 46 | // 47 | // Returns: 48 | // - bool: true if the error message is found, otherwise false. 49 | func (e *APIError) HasErrorMessage(message string) bool { 50 | for _, err := range e.Errors { 51 | if err.Message == message { 52 | return true 53 | } 54 | } 55 | return false 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/dev-docker.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish Dev Docker image 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | paths: 8 | - '**.go' 9 | - 'go.mod' 10 | - 'go.sum' 11 | - 'Dockerfile' 12 | - 'entrypoint.sh' 13 | - '.github/workflows/dev-docker.yml' 14 | pull_request: 15 | branches: 16 | - dev 17 | paths: 18 | - '**.go' 19 | - 'go.mod' 20 | - 'go.sum' 21 | - 'Dockerfile' 22 | - 'entrypoint.sh' 23 | - '.github/workflows/dev-docker.yml' 24 | workflow_dispatch: 25 | 26 | env: 27 | REGISTRY: ghcr.io 28 | IMAGE_NAME: ${{ github.repository }} 29 | 30 | jobs: 31 | build-and-push: 32 | runs-on: ubuntu-latest 33 | permissions: 34 | contents: read 35 | packages: write 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v4 40 | 41 | - name: Set up Docker Buildx 42 | uses: docker/setup-buildx-action@v3 43 | 44 | - name: Log in to GitHub Container Registry 45 | uses: docker/login-action@v3 46 | with: 47 | registry: ${{ env.REGISTRY }} 48 | username: ${{ github.actor }} 49 | password: ${{ secrets.GITHUB_TOKEN }} 50 | 51 | - name: Extract metadata 52 | id: meta 53 | uses: docker/metadata-action@v5 54 | with: 55 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 56 | tags: | 57 | type=ref,event=pr 58 | type=raw,value=dev 59 | type=sha,prefix=pr- 60 | 61 | - name: Build and push 62 | uses: docker/build-push-action@v5 63 | with: 64 | context: . 65 | push: true 66 | tags: ${{ steps.meta.outputs.tags }} 67 | labels: ${{ steps.meta.outputs.labels }} 68 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish Release Docker image 2 | on: 3 | push: 4 | tags: 5 | - '[0-9]*.[0-9]*' 6 | env: 7 | REGISTRY: ghcr.io 8 | IMAGE_NAME: ${{ github.repository }} 9 | jobs: 10 | build-and-push: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | packages: write 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | - name: Extract tag name 19 | id: tag 20 | run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 21 | - name: Set up QEMU 22 | uses: docker/setup-qemu-action@v3 23 | - name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v3 25 | - name: Log in to GitHub Container Registry 26 | uses: docker/login-action@v3 27 | with: 28 | registry: ${{ env.REGISTRY }} 29 | username: ${{ github.actor }} 30 | password: ${{ secrets.GITHUB_TOKEN }} 31 | - name: Extract metadata 32 | id: meta 33 | uses: docker/metadata-action@v5 34 | with: 35 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 36 | tags: | 37 | type=raw,value=latest 38 | type=raw,value=${{ steps.tag.outputs.VERSION }} 39 | - name: Update version in Go code 40 | run: | 41 | sed -i "s/version = \"dev\"/version = \"${{ steps.tag.outputs.VERSION }}\"/g" cmd/version.go 42 | - name: Build and push 43 | uses: docker/build-push-action@v5 44 | with: 45 | context: . 46 | platforms: linux/amd64,linux/arm64 47 | push: true 48 | tags: ${{ steps.meta.outputs.tags }} 49 | labels: ${{ steps.meta.outputs.labels }} 50 | build-args: | 51 | VERSION=${{ steps.tag.outputs.VERSION }} 52 | -------------------------------------------------------------------------------- /models/register.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Registration struct { 4 | Key string `json:"key"` 5 | InstallID string `json:"install_id"` 6 | FcmToken string `json:"fcm_token"` 7 | Tos string `json:"tos"` 8 | Model string `json:"model"` 9 | Serial string `json:"serial_number"` 10 | OsVersion string `json:"os_version"` 11 | KeyType string `json:"key_type"` 12 | TunType string `json:"tunnel_type"` 13 | Locale string `json:"locale"` 14 | } 15 | 16 | type AccountData struct { 17 | ID string `json:"id"` 18 | Type string `json:"type"` 19 | Model string `json:"model"` 20 | Name string `json:"name"` 21 | Key string `json:"key"` 22 | KeyType string `json:"key_type"` 23 | TunType string `json:"tunnel_type"` 24 | Account Account `json:"account"` 25 | Config Config `json:"config"` 26 | // WarpEnabled not set for ZeroTier 27 | WarpEnabled bool `json:"warp_enabled,omitempty"` 28 | // Waitlist not set for ZeroTier 29 | Waitlist bool `json:"waitlist_enabled,omitempty"` 30 | Created string `json:"created"` 31 | Updated string `json:"updated"` 32 | // Tos not set for ZeroTier 33 | Tos string `json:"tos,omitempty"` 34 | // Place not set for ZeroTier 35 | Place int `json:"place,omitempty"` 36 | Locale string `json:"locale"` 37 | // Enabled not set for ZeroTier 38 | Enabled bool `json:"enabled,omitempty"` 39 | InstallID string `json:"install_id"` 40 | // Token only set for /reg call 41 | Token string `json:"token,omitempty"` 42 | FcmToken string `json:"fcm_token"` 43 | // SerialNumber not set for ZeroTier 44 | SerialNumber string `json:"serial_number,omitempty"` 45 | Policy Policy `json:"policy"` 46 | } 47 | 48 | type Account struct { 49 | ID string `json:"id"` 50 | AccountType string `json:"account_type"` 51 | // Created not set for ZeroTier 52 | Created string `json:"created,omitempty"` 53 | // Updated not set for ZeroTier 54 | Updated string `json:"updated,omitempty"` 55 | // Managed only set for ZeroTier 56 | Managed string `json:"managed,omitempty"` 57 | // Organization only set for ZeroTier 58 | Organization string `json:"organization,omitempty"` 59 | // PremiumData not set for ZeroTier 60 | PremiumData int `json:"premium_data,omitempty"` 61 | // Quota not set for ZeroTier 62 | Quota int `json:"quota,omitempty"` 63 | // WarpPlus not set for ZeroTier 64 | WarpPlus bool `json:"warp_plus,omitempty"` 65 | // ReferralCode not set for ZeroTier 66 | ReferralCount int `json:"referral_count,omitempty"` 67 | // ReferralRenewalCount not set for ZeroTier 68 | ReferralRenewalCount int `json:"referral_renewal_countdown,omitempty"` 69 | // Role not set for ZeroTier 70 | Role string `json:"role,omitempty"` 71 | // License not set for ZeroTier 72 | License string `json:"license,omitempty"` 73 | } 74 | 75 | type Config struct { 76 | ClientID string `json:"client_id"` 77 | Peers []Peer `json:"peers"` 78 | Interface struct { 79 | Addresses struct { 80 | V4 string `json:"v4"` 81 | V6 string `json:"v6"` 82 | } `json:"addresses"` 83 | } `json:"interface"` 84 | Services struct { 85 | HTTPProxy string `json:"http_proxy"` 86 | } `json:"services"` 87 | } 88 | 89 | type Peer struct { 90 | PublicKey string `json:"public_key"` 91 | Endpoint struct { 92 | V4 string `json:"v4"` 93 | V6 string `json:"v6"` 94 | Host string `json:"host"` 95 | Ports []int `json:"ports"` 96 | } `json:"endpoint"` 97 | } 98 | 99 | type Policy struct { 100 | TunnelProtocol string `json:"tunnel_protocol"` 101 | // TODO: add ZeroTier fields 102 | } 103 | -------------------------------------------------------------------------------- /.github/workflows/build-binaries.yml: -------------------------------------------------------------------------------- 1 | name: Build Binaries 2 | on: 3 | push: 4 | tags: 5 | - '[0-9]*.[0-9]*' 6 | 7 | jobs: 8 | build: 9 | name: Build Binary 10 | runs-on: ${{ matrix.os }} 11 | permissions: 12 | contents: write 13 | strategy: 14 | matrix: 15 | os: [windows-latest, ubuntu-latest, macos-latest] 16 | arch: [amd64, arm64] 17 | include: 18 | - os: windows-latest 19 | arch: amd64 20 | binary_name: usque.exe 21 | asset_name: usque-windows-amd64.exe 22 | - os: windows-latest 23 | arch: arm64 24 | binary_name: usque.exe 25 | asset_name: usque-windows-arm64.exe 26 | - os: ubuntu-latest 27 | arch: amd64 28 | binary_name: usque 29 | asset_name: usque-linux-amd64 30 | - os: ubuntu-latest 31 | arch: arm64 32 | binary_name: usque 33 | asset_name: usque-linux-arm64 34 | - os: macos-latest 35 | arch: amd64 36 | binary_name: usque 37 | asset_name: usque-darwin-amd64 38 | - os: macos-latest 39 | arch: arm64 40 | binary_name: usque 41 | asset_name: usque-darwin-arm64 42 | 43 | steps: 44 | - name: Check out code 45 | uses: actions/checkout@v4 46 | 47 | - name: Set up Go 48 | uses: actions/setup-go@v5 49 | with: 50 | go-version-file: 'go.mod' 51 | cache: true 52 | 53 | - name: Extract tag name 54 | id: tag 55 | run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 56 | 57 | - name: Update version in Go code 58 | run: | 59 | if [[ "$RUNNER_OS" == "macOS" ]]; then 60 | sed -i '' "s/version = \"dev\"/version = \"${{ steps.tag.outputs.VERSION }}\"/g" cmd/version.go 61 | else 62 | sed -i "s/version = \"dev\"/version = \"${{ steps.tag.outputs.VERSION }}\"/g" cmd/version.go 63 | fi 64 | shell: bash 65 | 66 | - name: Build Binary 67 | env: 68 | GOARCH: ${{ matrix.arch }} 69 | run: | 70 | go build -v -o ${{ matrix.binary_name }} 71 | 72 | - uses: actions/upload-artifact@v4 73 | with: 74 | name: ${{ matrix.asset_name }} 75 | path: ${{ matrix.binary_name }} 76 | retention-days: 5 77 | 78 | release: 79 | name: Create Release 80 | runs-on: ubuntu-latest 81 | needs: build 82 | permissions: 83 | contents: write 84 | steps: 85 | - name: Checkout repository 86 | uses: actions/checkout@v4 87 | 88 | - name: Extract tag name 89 | id: tag 90 | run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 91 | 92 | - name: Download all artifacts 93 | uses: actions/download-artifact@v4 94 | with: 95 | path: ./artifacts 96 | 97 | - name: Create Release and Upload Assets 98 | env: 99 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 100 | run: | 101 | # Create release 102 | gh release create ${{ steps.tag.outputs.VERSION }} \ 103 | --title "Release ${{ steps.tag.outputs.VERSION }}" \ 104 | --generate-notes 105 | 106 | # Upload all binary assets 107 | for dir in ./artifacts/*/; do 108 | asset_name=$(basename "$dir") 109 | file_path=$(find "$dir" -type f | head -1) 110 | if [ -f "$file_path" ]; then 111 | echo "Uploading $asset_name from $file_path" 112 | gh release upload ${{ steps.tag.outputs.VERSION }} "$file_path#$asset_name" 113 | fi 114 | done 115 | -------------------------------------------------------------------------------- /api/resolver.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/netip" 8 | "sync" 9 | "time" 10 | 11 | "golang.zx2c4.com/wireguard/tun/netstack" 12 | ) 13 | 14 | // DNSCacheEntry 表示缓存中的一个条目 15 | type DNSCacheEntry struct { 16 | IP net.IP 17 | ExpiresAt time.Time 18 | } 19 | 20 | // CachingDNSResolver 实现了带缓存的DNS解析器 21 | type CachingDNSResolver struct { 22 | // DNS服务器地址 23 | DNSServer string 24 | // 缓存过期时间(秒) 25 | CacheTTL int 26 | // 缓存 27 | cache map[string]DNSCacheEntry 28 | cacheLock sync.RWMutex 29 | } 30 | 31 | // NewCachingDNSResolver 创建一个新的缓存DNS解析器 32 | // dnsServer: DNS服务器地址,如 "8.8.8.8:53" 33 | // cacheTTLSeconds: 缓存有效期(秒) 34 | func NewCachingDNSResolver(dnsServer string, cacheTTLSeconds int) *CachingDNSResolver { 35 | if cacheTTLSeconds <= 0 { 36 | cacheTTLSeconds = 600 // 默认10分钟 37 | } 38 | 39 | if dnsServer == "" { 40 | dnsServer = "8.8.8.8:53" // 默认使用谷歌DNS 41 | } 42 | 43 | return &CachingDNSResolver{ 44 | DNSServer: dnsServer, 45 | CacheTTL: cacheTTLSeconds, 46 | cache: make(map[string]DNSCacheEntry), 47 | } 48 | } 49 | 50 | type dnsLookupResult struct { 51 | ip net.IP 52 | err error 53 | } 54 | 55 | // Resolve 实现NameResolver接口,解析域名为IP地址 56 | func (r *CachingDNSResolver) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) { 57 | // 先检查缓存 58 | r.cacheLock.RLock() 59 | entry, exists := r.cache[name] 60 | now := time.Now() 61 | cacheHit := exists && now.Before(entry.ExpiresAt) 62 | r.cacheLock.RUnlock() 63 | 64 | // 如果缓存中存在且未过期,直接返回 65 | if cacheHit { 66 | return ctx, entry.IP, nil 67 | } 68 | 69 | // 使用单独锁来防止对同一域名的并发DNS查询,实现"查询合并" 70 | resultChan := make(chan dnsLookupResult, 1) 71 | 72 | // 缓存不存在或已过期,进行实际的DNS查询 73 | // 这里可以添加错误重试逻辑 74 | go func() { 75 | resolver := &net.Resolver{ 76 | PreferGo: true, 77 | Dial: func(ctx context.Context, network, address string) (net.Conn, error) { 78 | d := net.Dialer{Timeout: time.Second * 5} 79 | return d.DialContext(ctx, "udp", r.DNSServer) 80 | }, 81 | } 82 | 83 | ips, err := resolver.LookupIP(ctx, "ip", name) 84 | if err != nil { 85 | resultChan <- dnsLookupResult{nil, err} 86 | return 87 | } 88 | 89 | if len(ips) == 0 { 90 | resultChan <- dnsLookupResult{nil, net.ErrClosed} 91 | return 92 | } 93 | 94 | resultChan <- dnsLookupResult{ips[0], nil} 95 | }() 96 | 97 | // 等待DNS查询完成或上下文取消 98 | select { 99 | case <-ctx.Done(): 100 | return ctx, nil, ctx.Err() 101 | case result := <-resultChan: 102 | if result.err != nil { 103 | return ctx, nil, result.err 104 | } 105 | 106 | // 更新缓存 107 | r.cacheLock.Lock() 108 | r.cache[name] = DNSCacheEntry{ 109 | IP: result.ip, 110 | ExpiresAt: now.Add(time.Duration(r.CacheTTL) * time.Second), 111 | } 112 | r.cacheLock.Unlock() 113 | 114 | return ctx, result.ip, nil 115 | } 116 | } 117 | 118 | // ClearCache 清除DNS缓存 119 | func (r *CachingDNSResolver) ClearCache() { 120 | r.cacheLock.Lock() 121 | defer r.cacheLock.Unlock() 122 | r.cache = make(map[string]DNSCacheEntry) 123 | } 124 | 125 | // TunnelDNSResolver implements a DNS resolver that uses the provided DNS servers inside a MASQUE tunnel. 126 | type TunnelDNSResolver struct { 127 | // tunNet is the network stack for the tunnel you want to use for DNS resolution. 128 | tunNet *netstack.Net 129 | // dnsAddrs is the list of DNS servers to use for resolution. 130 | dnsAddrs []netip.Addr 131 | // timeout is the timeout for DNS queries on a specific server before trying the next one. 132 | timeout time.Duration 133 | } 134 | 135 | // NewTunnelDNSResolver creates a new TunnelDNSResolver. 136 | // 137 | // Parameters: 138 | // - tunNet: *netstack.Net - The network stack for the tunnel. 139 | // - dnsAddrs: []netip.Addr - The list of DNS servers to use for resolution. 140 | // - timeout: time.Duration - The timeout for DNS queries on a specific server before trying the next one. 141 | // 142 | // Returns: 143 | // - *TunnelDNSResolver: The newly created TunnelDNSResolver. 144 | func NewTunnelDNSResolver(tunNet *netstack.Net, dnsAddrs []netip.Addr, timeout time.Duration) *TunnelDNSResolver { 145 | return &TunnelDNSResolver{ 146 | tunNet: tunNet, 147 | dnsAddrs: dnsAddrs, 148 | timeout: timeout, 149 | } 150 | } 151 | 152 | // Resolve performs a DNS lookup using the provided DNS resolvers. 153 | // It tries each resolver in order until one succeeds. 154 | // 155 | // Parameters: 156 | // - ctx: context.Context - The context for the DNS lookup. 157 | // - name: string - The domain name to resolve. 158 | // 159 | // Returns: 160 | // - context.Context: The context for the DNS lookup. 161 | // - net.IP: The resolved IP address. 162 | // - error: An error if the lookup fails. 163 | func (r TunnelDNSResolver) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) { 164 | var lastErr error 165 | 166 | for _, dnsAddr := range r.dnsAddrs { 167 | dnsHost := net.JoinHostPort(dnsAddr.String(), "53") 168 | 169 | resolver := &net.Resolver{ 170 | PreferGo: true, 171 | Dial: func(ctx context.Context, network, address string) (net.Conn, error) { 172 | return r.tunNet.DialContext(ctx, "udp", dnsHost) 173 | }, 174 | } 175 | 176 | ips, err := resolver.LookupIP(ctx, "ip", name) 177 | if err == nil && len(ips) > 0 { 178 | return ctx, ips[0], nil 179 | } 180 | lastErr = err 181 | } 182 | 183 | return ctx, nil, fmt.Errorf("all DNS servers failed: %v", lastErr) 184 | } 185 | -------------------------------------------------------------------------------- /api/masque.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "crypto/ecdsa" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "net/http" 12 | 13 | connectip "github.com/Diniboy1123/connect-ip-go" 14 | "github.com/quic-go/quic-go" 15 | "github.com/quic-go/quic-go/http3" 16 | "github.com/yosida95/uritemplate/v3" 17 | ) 18 | 19 | // PrepareTlsConfig creates a TLS configuration using the provided certificate and SNI (Server Name Indication). 20 | // It also verifies the peer's public key against the provided public key. 21 | // 22 | // Parameters: 23 | // - privKey: *ecdsa.PrivateKey - The private key to use for TLS authentication. 24 | // - peerPubKey: *ecdsa.PublicKey - The endpoint's public key to pin to. 25 | // - cert: [][]byte - The certificate chain to use for TLS authentication. 26 | // - sni: string - The Server Name Indication (SNI) to use. 27 | // 28 | // Returns: 29 | // - *tls.Config: A TLS configuration for secure communication. 30 | // - error: An error if TLS setup fails. 31 | func PrepareTlsConfig(privKey *ecdsa.PrivateKey, peerPubKey *ecdsa.PublicKey, cert [][]byte, sni string) (*tls.Config, error) { 32 | tlsConfig := &tls.Config{ 33 | Certificates: []tls.Certificate{ 34 | { 35 | Certificate: cert, 36 | PrivateKey: privKey, 37 | }, 38 | }, 39 | ServerName: sni, 40 | NextProtos: []string{http3.NextProtoH3}, 41 | // WARN: SNI is usually not for the endpoint, so we must skip verification 42 | InsecureSkipVerify: true, 43 | // we pin to the endpoint public key 44 | VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { 45 | if len(rawCerts) == 0 { 46 | return nil 47 | } 48 | 49 | cert, err := x509.ParseCertificate(rawCerts[0]) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | if _, ok := cert.PublicKey.(*ecdsa.PublicKey); !ok { 55 | // we only support ECDSA 56 | // TODO: don't hardcode cert type in the future 57 | // as backend can start using different cert types 58 | return x509.ErrUnsupportedAlgorithm 59 | } 60 | 61 | if !cert.PublicKey.(*ecdsa.PublicKey).Equal(peerPubKey) { 62 | // reason is incorrect, but the best I could figure 63 | // detail explains the actual reason 64 | 65 | //10 is NoValidChains, but we support go1.22 where it's not defined 66 | return x509.CertificateInvalidError{Cert: cert, Reason: 10, Detail: "remote endpoint has a different public key than what we trust in config.json"} 67 | } 68 | 69 | return nil 70 | }, 71 | } 72 | 73 | return tlsConfig, nil 74 | } 75 | 76 | // ConnectTunnel establishes a QUIC connection and sets up a Connect-IP tunnel with the provided endpoint. 77 | // Endpoint address is used to check whether the authentication/connection is successful or not. 78 | // Requires modified connect-ip-go for now to support Cloudflare's non RFC compliant implementation. 79 | // 80 | // Parameters: 81 | // - ctx: context.Context - The QUIC TLS context. 82 | // - tlsConfig: *tls.Config - The TLS configuration for secure communication. 83 | // - quicConfig: *quic.Config - The QUIC configuration settings. 84 | // - connectUri: string - The URI template for the Connect-IP request. 85 | // - endpoint: *net.UDPAddr - The UDP address of the QUIC server. 86 | // 87 | // Returns: 88 | // - *net.UDPConn: The UDP connection used for the QUIC session. 89 | // - *http3.Transport: The HTTP/3 transport used for initial request. 90 | // - *connectip.Conn: The Connect-IP connection instance. 91 | // - *http.Response: The response from the Connect-IP handshake. 92 | // - error: An error if the connection setup fails. 93 | func ConnectTunnel(ctx context.Context, tlsConfig *tls.Config, quicConfig *quic.Config, connectUri string, endpoint *net.UDPAddr) (*net.UDPConn, *http3.Transport, *connectip.Conn, *http.Response, error) { 94 | var udpConn *net.UDPConn 95 | var err error 96 | if endpoint.IP.To4() == nil { 97 | udpConn, err = net.ListenUDP("udp", &net.UDPAddr{ 98 | IP: net.IPv6zero, 99 | Port: 0, 100 | }) 101 | } else { 102 | udpConn, err = net.ListenUDP("udp", &net.UDPAddr{ 103 | IP: net.IPv4zero, 104 | Port: 0, 105 | }) 106 | } 107 | if err != nil { 108 | return udpConn, nil, nil, nil, err 109 | } 110 | 111 | conn, err := quic.Dial( 112 | ctx, 113 | udpConn, 114 | endpoint, 115 | tlsConfig, 116 | quicConfig, 117 | ) 118 | if err != nil { 119 | return udpConn, nil, nil, nil, err 120 | } 121 | 122 | tr := &http3.Transport{ 123 | EnableDatagrams: true, 124 | AdditionalSettings: map[uint64]uint64{ 125 | // official client still sends this out as well, even though 126 | // it's deprecated, see https://datatracker.ietf.org/doc/draft-ietf-masque-h3-datagram/00/ 127 | // SETTINGS_H3_DATAGRAM_00 = 0x0000000000000276 128 | // https://github.com/cloudflare/quiche/blob/7c66757dbc55b8d0c3653d4b345c6785a181f0b7/quiche/src/h3/frame.rs#L46 129 | 0x276: 1, 130 | }, 131 | DisableCompression: true, 132 | } 133 | 134 | hconn := tr.NewClientConn(conn) 135 | 136 | additionalHeaders := http.Header{ 137 | "User-Agent": []string{""}, 138 | } 139 | 140 | template := uritemplate.MustNew(connectUri) 141 | ipConn, rsp, err := connectip.Dial(ctx, hconn, template, "cf-connect-ip", additionalHeaders, true) 142 | if err != nil { 143 | if err.Error() == "CRYPTO_ERROR 0x131 (remote): tls: access denied" { 144 | return udpConn, nil, nil, nil, errors.New("login failed! Please double-check if your tls key and cert is enrolled in the Cloudflare Access service") 145 | } 146 | return udpConn, nil, nil, nil, fmt.Errorf("failed to dial connect-ip: %v", err) 147 | } 148 | 149 | return udpConn, tr, ipConn, rsp, nil 150 | } 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # USCF (Modified from Usque) 3 | Before using this tool, You need to agree the code License and CloudFlare Tos. 4 | 5 | USCF is a 3-party experiment tool that connects to Cloudflare Warp using a unique QUIC-based protocol. This lightweight and high-performance tool provides a simple and easy-to-use SOCKS5 proxy for secure connections to Warp. 6 | 7 | This is a tool modified from [Usque](https://github.com/Diniboy1123/usque), my branch mainly improve performace like stable memory usage or high cocurrent efficiency. 8 | 9 | ## Features 10 | 11 | - Small, Lightweight, One-Command Automatic Deploy, Simple To Use 12 | - Faster and more portable than using Wireguard Warp 13 | - High Performance under connection pressure 14 | - Docker containerization support 15 | 16 | 17 | 18 | ### Build from Source 19 | 20 | 21 | ```bash 22 | # Clone the repository 23 | git clone https://github.com/HynoR/uscf.git 24 | cd uscf 25 | 26 | # Build 27 | go build -o uscf . 28 | ``` 29 | 30 | ## Usage 31 | 32 | ### First Use (Automatic Registration) 33 | 34 | Before you use this tool, you must accept and follow [Cloudflare TOS](https://www.cloudflare.com/application/terms/)!!! 35 | 36 | The first time you run USCF, it will automatically register a Cloudflare Warp account and create a configuration file: 37 | 38 | ```bash 39 | ./uscf proxy -b -u -w -p -c 40 | ``` 41 | 42 | ### Use Existing Configuration 43 | 44 | If you already have a configuration file, run directly: 45 | 46 | ```bash 47 | ./uscf proxy -c config.json 48 | ``` 49 | 50 | 51 | ## Docker Deployment 52 | 53 | ### Build Docker Image 54 | 55 | ```bash 56 | docker build -t uscf:latest . 57 | ``` 58 | 59 | ### RUN 60 | 61 | ``` 62 | docker run -d --name uscf --network=host -v /etc/uscf/:/app/etc/ --log-driver json-file --log-opt max-size=3m --restart on-failure --privileged uscf 63 | ``` 64 | 65 | 66 | ## Configuration File Description 67 | 68 | USCF uses a JSON format configuration file. The default configuration file path is `config.json` in the current directory. 69 | 70 | ### Configuration Example 71 | 72 | After Automatic Registration, You would get a config.json like the example below, you can edit items and then restart your program to apply them. 73 | The Config file is merge from usque's flags and configs, You can find the description of config items from usque. 74 | 75 | ```json 76 | { 77 | "private_key": "BASE64 encoded ECDSA private key(Auto Generate)", 78 | "endpoint_v4": "(Auto Generate)", 79 | "endpoint_v6": "(Auto Generate)", 80 | "endpoint_pub_key": "PEM encoded ECDSA public key(Auto Generate)", 81 | "license": "License key(Auto Generate)", 82 | "id": "Unique device identifier(Auto Generate)", 83 | "access_token": "API access token(Auto Generate)", 84 | "ipv4": "Assigned IPv4 address(Auto Generate)", 85 | "ipv6": "Assigned IPv6 address(Auto Generate)", 86 | "socks": { 87 | "bind_address": "0.0.0.0", 88 | "port": "2333", 89 | "username": "", 90 | "password": "", 91 | "connect_port": 443, 92 | "dns": [ 93 | "1.1.1.1", 94 | "8.8.8.8" 95 | ], 96 | "dns_timeout": 2000000000, 97 | "use_ipv6": false, 98 | "no_tunnel_ipv4": false, 99 | "no_tunnel_ipv6": false, 100 | "sni_address": "", 101 | "keepalive_period": 30000000000, 102 | "mtu": 1280, 103 | "initial_packet_size": 1242, 104 | "reconnect_delay": 1000000000, 105 | "connection_timeout": 30000000000, 106 | "idle_timeout": 300000000000 107 | }, 108 | "registration": { 109 | "device_name": "Device name" 110 | } 111 | } 112 | ``` 113 | 114 | 115 | 116 | ## Reset Configuration 117 | 118 | If you need to reset the SOCKS5 proxy configuration to default values, you can use the following command: 119 | 120 | ```bash 121 | ./uscf proxy --reset-config 122 | ``` 123 | 124 | ## More Command Options 125 | 126 | ### proxy Command 127 | 128 | ```bash 129 | ./uscf proxy [flags] 130 | ``` 131 | 132 | Available flags: 133 | - `--locale string`: Locale used during registration (default "en_US") 134 | - `--model string`: Device model used during registration (defaults to automatic detection based on the system) 135 | - `--name string`: Device name used during registration 136 | - `--accept-tos`: Automatically accept Cloudflare Terms of Service (default true) 137 | - `--jwt string`: Team token (optional) 138 | - `--reset-config`: Reset SOCKS5 configuration to default values 139 | - `-c, --config string`: Configuration file path (default "config.json") 140 | 141 | ## Connection Example 142 | 143 | Once the USCF proxy service is running, you can configure applications to use the SOCKS5 proxy: 144 | 145 | ``` 146 | Proxy Address: 127.0.0.1 (or the bind_address you set) 147 | Proxy Port: 2333 (or the port you configured) 148 | Proxy Type: SOCKS5 149 | Authentication Information: If you set username and password in the configuration, you need to provide them 150 | ``` 151 | 152 | ## Disclaimer 153 | 154 | Please do NOT use this tool for abuse. At the end of the day you hurt Cloudflare, which is probably unfair as you get this stuff even for free, secondly you will most likely get this tool sanctioned and ruin the fun for everyone. 155 | 156 | The tool mimics certain properties of the official clients, those are mostly done for stability and compatibility reasons. I never intended to make this tool indistinguishable from the official clients. That means if they want to detect this tool, they can. I am not responsible for any consequences that may arise from using this tool. That is absolutely your own responsibility. I am not responsible for any damage that may occur to your system or your network. This tool is provided as is without any guarantees. Use at your own risk. 157 | 158 | 159 | ## License 160 | 161 | This project is open source under the MIT License. 162 | -------------------------------------------------------------------------------- /api/cloudflare.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "time" 11 | 12 | "github.com/HynoR/uscf/internal" 13 | "github.com/HynoR/uscf/models" 14 | ) 15 | 16 | // Register creates a new user account by registering a WireGuard public key and generating a random Android-like device identifier. 17 | // The WireGuard private key isn't stored anywhere, therefore it won't be usable. It's sole purpose is to mimic the Android app's registration process. 18 | // 19 | // This function sends a POST request to the API to register a new user and returns the created account data. 20 | // 21 | // Parameters: 22 | // - model: string - The device model string to register. (e.g., "PC") 23 | // - locale: string - The user's locale. (e.g., "en-US") 24 | // - jwt: string - Team token to register. 25 | // - acceptTos: bool - Whether the user accepts the Terms of Service (TOS). If false, the user will be prompted to accept. 26 | // 27 | // Returns: 28 | // - models.AccountData: The account data returned from the registration process. 29 | // - error: An error if registration fails at any step. 30 | // 31 | // Example: 32 | // 33 | // account, err := Register("PC", "en-US", "", false) 34 | // if err != nil { 35 | // log.Fatalf("Registration failed: %v", err) 36 | // } 37 | func Register(model, locale, jwt string, acceptTos bool) (models.AccountData, error) { 38 | wgKey, err := internal.GenerateRandomWgPubkey() 39 | if err != nil { 40 | return models.AccountData{}, fmt.Errorf("failed to generate wg key: %v", err) 41 | } 42 | serial, err := internal.GenerateRandomAndroidSerial() 43 | if err != nil { 44 | return models.AccountData{}, fmt.Errorf("failed to generate serial: %v", err) 45 | } 46 | 47 | if !acceptTos { 48 | fmt.Print("You must accept the Terms of Service (https://www.cloudflare.com/application/terms/) to register. Do you agree? (y/n): ") 49 | var response string 50 | if _, err := fmt.Scanln(&response); err != nil { 51 | return models.AccountData{}, fmt.Errorf("failed to read user input: %v", err) 52 | } 53 | if response != "y" { 54 | return models.AccountData{}, fmt.Errorf("user did not accept TOS") 55 | } 56 | } 57 | 58 | data := models.Registration{ 59 | Key: wgKey, 60 | InstallID: "", 61 | FcmToken: "", 62 | Tos: internal.TimeAsCfString(time.Now()), 63 | Model: model, 64 | Serial: serial, 65 | OsVersion: "", 66 | KeyType: internal.KeyTypeWg, 67 | TunType: internal.TunTypeWg, 68 | Locale: locale, 69 | } 70 | 71 | jsonData, err := json.Marshal(data) 72 | if err != nil { 73 | return models.AccountData{}, fmt.Errorf("failed to marshal json: %v", err) 74 | } 75 | 76 | req, err := http.NewRequest("POST", internal.ApiUrl+"/"+internal.ApiVersion+"/reg", bytes.NewBuffer(jsonData)) 77 | if err != nil { 78 | return models.AccountData{}, fmt.Errorf("failed to create request: %v", err) 79 | } 80 | 81 | for k, v := range internal.Headers { 82 | req.Header.Set(k, v) 83 | } 84 | 85 | if jwt != "" { 86 | req.Header.Set("CF-Access-Jwt-Assertion", jwt) 87 | } 88 | 89 | resp, err := http.DefaultClient.Do(req) 90 | if err != nil { 91 | return models.AccountData{}, fmt.Errorf("failed to send request: %v", err) 92 | } 93 | defer resp.Body.Close() 94 | 95 | if resp.StatusCode != http.StatusOK { 96 | return models.AccountData{}, fmt.Errorf("failed to register: %v", resp.Status) 97 | } 98 | 99 | var accountData models.AccountData 100 | if err := json.NewDecoder(resp.Body).Decode(&accountData); err != nil { 101 | return models.AccountData{}, fmt.Errorf("failed to decode response: %v", err) 102 | } 103 | 104 | return accountData, nil 105 | } 106 | 107 | // EnrollKey updates an existing user account with a new MASQUE public key. 108 | // 109 | // This function sends a PATCH request to update the user's account with a new key. 110 | // 111 | // Parameters: 112 | // - accountData: models.AccountData - The account data of the user being updated. 113 | // - pubKey: []byte - The new MASQUE public key in binary format. 114 | // - deviceName: string - The name of the device to enroll. (optional) 115 | // 116 | // Returns: 117 | // - models.AccountData: The updated account data. 118 | // - error: An error if the update process fails. 119 | // 120 | // Example: 121 | // 122 | // updatedAccount, apiErr, err := EnrollKey(account, pubKey, "PC") 123 | // if err != nil { 124 | // log.Fatalf("Key enrollment failed: %v", err) 125 | // } 126 | func EnrollKey(accountData models.AccountData, pubKey []byte, deviceName string) (models.AccountData, *models.APIError, error) { 127 | deviceUpdate := models.DeviceUpdate{ 128 | Key: base64.StdEncoding.EncodeToString(pubKey), 129 | KeyType: internal.KeyTypeMasque, 130 | TunType: internal.TunTypeMasque, 131 | } 132 | 133 | if deviceName != "" { 134 | deviceUpdate.Name = deviceName 135 | } 136 | 137 | jsonData, err := json.Marshal(deviceUpdate) 138 | if err != nil { 139 | return models.AccountData{}, nil, fmt.Errorf("failed to marshal json: %v", err) 140 | } 141 | 142 | req, err := http.NewRequest("PATCH", internal.ApiUrl+"/"+internal.ApiVersion+"/reg/"+accountData.ID, bytes.NewBuffer(jsonData)) 143 | if err != nil { 144 | return models.AccountData{}, nil, fmt.Errorf("failed to create request: %v", err) 145 | } 146 | 147 | for k, v := range internal.Headers { 148 | req.Header.Set(k, v) 149 | } 150 | req.Header.Set("Authorization", "Bearer "+accountData.Token) 151 | 152 | resp, err := http.DefaultClient.Do(req) 153 | if err != nil { 154 | return models.AccountData{}, nil, fmt.Errorf("failed to send request: %v", err) 155 | } 156 | defer resp.Body.Close() 157 | 158 | body, err := io.ReadAll(resp.Body) 159 | if err != nil { 160 | return models.AccountData{}, nil, fmt.Errorf("failed to read response body: %v", err) 161 | } 162 | 163 | if resp.StatusCode != http.StatusOK { 164 | var apiErr models.APIError 165 | if err := json.Unmarshal(body, &apiErr); err != nil { 166 | return models.AccountData{}, nil, fmt.Errorf("failed to parse error response: %v", err) 167 | } 168 | return models.AccountData{}, &apiErr, fmt.Errorf("failed to update: %s", resp.Status) 169 | } 170 | 171 | if err := json.Unmarshal(body, &accountData); err != nil { 172 | return models.AccountData{}, nil, fmt.Errorf("failed to decode response: %v", err) 173 | } 174 | 175 | return accountData, nil, nil 176 | } 177 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Diniboy1123/connect-ip-go v0.0.0-20250220050656-56698ca53ed4 h1:w5pJcAdMw/tasMbu5mKDwWgWlCzqj7U5h3E6cwwbbJA= 2 | github.com/Diniboy1123/connect-ip-go v0.0.0-20250220050656-56698ca53ed4/go.mod h1:kJdfLaWM/6v0+nmG7JgoicKqs+D31VAAh937Qq2pe+c= 3 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 4 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 5 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 6 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0= 11 | github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= 12 | github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= 13 | github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 14 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 15 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 16 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 17 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 18 | github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= 19 | github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 20 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 21 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 22 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= 23 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 24 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 25 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 26 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 27 | github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= 28 | github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= 29 | github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= 30 | github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= 31 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 32 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 33 | github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= 34 | github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= 35 | github.com/quic-go/quic-go v0.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc= 36 | github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ= 37 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 38 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 39 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 40 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 41 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 42 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 43 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 44 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 45 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 46 | github.com/things-go/go-socks5 v0.0.6 h1:YjylIYZiND41szH4NzsVbx8aVDsS/Y8ps3QYPwQvqnI= 47 | github.com/things-go/go-socks5 v0.0.6/go.mod h1:RF6tRutwNWzISbPfiDEChH/o1aDfRv+cXDYn2a2qkK4= 48 | github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= 49 | github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= 50 | go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= 51 | go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 52 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 53 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 54 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= 55 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 56 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 57 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 58 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 59 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 60 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 61 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 62 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 63 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 64 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 65 | golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= 66 | golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 67 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 68 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 69 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= 70 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= 71 | golang.zx2c4.com/wireguard v0.0.0-20250505131008-436f7fdc1670 h1:lvCs+t4iJfAyIbkYw1MUjsQw2eL04Pw9Dym75u3SnTs= 72 | golang.zx2c4.com/wireguard v0.0.0-20250505131008-436f7fdc1670/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= 73 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 74 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 75 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 76 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 77 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 78 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 79 | gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI= 80 | gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= 81 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/x509" 6 | "encoding/base64" 7 | "encoding/json" 8 | "encoding/pem" 9 | "fmt" 10 | "os" 11 | "time" 12 | ) 13 | 14 | // Config represents the application configuration structure, containing essential details such as keys, endpoints, and access tokens. 15 | type Config struct { 16 | // 连接信息 17 | PrivateKey string `json:"private_key"` // Base64-encoded ECDSA private key 18 | EndpointV4 string `json:"endpoint_v4"` // IPv4 address of the endpoint 19 | EndpointV6 string `json:"endpoint_v6"` // IPv6 address of the endpoint 20 | EndpointPubKey string `json:"endpoint_pub_key"` // PEM-encoded ECDSA public key of the endpoint to verify against 21 | License string `json:"license"` // Application license key 22 | ID string `json:"id"` // Device unique identifier 23 | AccessToken string `json:"access_token"` // Authentication token for API access 24 | IPv4 string `json:"ipv4"` // Assigned IPv4 address 25 | IPv6 string `json:"ipv6"` // Assigned IPv6 address 26 | 27 | // SOCKS代理配置 28 | Socks SocksConfig `json:"socks"` // SOCKS5代理相关配置 29 | 30 | // 注册信息 31 | Registration RegistrationInfo `json:"registration"` // 注册相关信息 32 | } 33 | 34 | // SocksConfig 包含SOCKS5代理相关的配置 35 | type SocksConfig struct { 36 | BindAddress string `json:"bind_address"` // 代理绑定的地址 37 | Port string `json:"port"` // 代理监听的端口 38 | Username string `json:"username"` // 代理认证的用户名 39 | Password string `json:"password"` // 代理认证的密码 40 | ConnectPort int `json:"connect_port"` // MASQUE连接使用的端口 41 | DNS []string `json:"dns"` // 在MASQUE隧道内使用的DNS服务器 42 | DNSTimeout time.Duration `json:"dns_timeout"` // DNS查询超时时间(超时后尝试下一个服务器) 43 | RemoteDNS bool `json:"remote_dns"` // 是否使用远程DNS(通过TUN隧道),false则使用本地DNS 44 | UseIPv6 bool `json:"use_ipv6"` // 是否使用IPv6进行MASQUE连接 45 | NoTunnelIPv4 bool `json:"no_tunnel_ipv4"` // 是否在MASQUE隧道内禁用IPv4 46 | NoTunnelIPv6 bool `json:"no_tunnel_ipv6"` // 是否在MASQUE隧道内禁用IPv6 47 | SNIAddress string `json:"sni_address"` // MASQUE连接使用的SNI地址 48 | KeepalivePeriod time.Duration `json:"keepalive_period"` // MASQUE连接的心跳周期 49 | MTU int `json:"mtu"` // MASQUE连接的MTU 50 | InitialPacketSize uint16 `json:"initial_packet_size"` // MASQUE连接的初始包大小 51 | ReconnectDelay time.Duration `json:"reconnect_delay"` // 重连尝试之间的延迟 52 | ConnectionTimeout time.Duration `json:"connection_timeout"` // 建立连接的超时时间 53 | IdleTimeout time.Duration `json:"idle_timeout"` // 空闲连接的超时时间 54 | } 55 | 56 | // RegistrationInfo 包含注册相关的信息 57 | type RegistrationInfo struct { 58 | DeviceName string `json:"device_name"` // 注册的设备名称 59 | } 60 | 61 | // AppConfig holds the global application configuration. 62 | var AppConfig Config 63 | 64 | // ConfigLoaded indicates whether the configuration has been successfully loaded. 65 | var ConfigLoaded bool 66 | 67 | // LoadConfig loads the application configuration from a JSON file. 68 | // 69 | // Parameters: 70 | // - configPath: string - The path to the configuration JSON file. 71 | // 72 | // Returns: 73 | // - error: An error if the configuration file cannot be loaded or parsed. 74 | func LoadConfig(configPath string) error { 75 | file, err := os.Open(configPath) 76 | if err != nil { 77 | return fmt.Errorf("failed to open config file: %v", err) 78 | } 79 | defer file.Close() 80 | 81 | decoder := json.NewDecoder(file) 82 | if err := decoder.Decode(&AppConfig); err != nil { 83 | return fmt.Errorf("failed to decode config file: %v", err) 84 | } 85 | 86 | // 如果Socks配置为空,设置默认值 87 | // 判断Socks配置是否已初始化(通过检查关键字段) 88 | if AppConfig.Socks.Port == "" && AppConfig.Socks.BindAddress == "" && len(AppConfig.Socks.DNS) == 0 { 89 | AppConfig.Socks = GetDefaultSocksConfig() 90 | } 91 | 92 | ConfigLoaded = true 93 | 94 | return nil 95 | } 96 | 97 | // GetDefaultSocksConfig 返回默认的SOCKS代理配置 98 | func GetDefaultSocksConfig() SocksConfig { 99 | return SocksConfig{ 100 | BindAddress: "127.0.0.1", 101 | Port: "1080", 102 | Username: "", 103 | Password: "", 104 | ConnectPort: 443, 105 | DNS: []string{"1.1.1.1", "8.8.8.8"}, 106 | DNSTimeout: 2 * time.Second, 107 | RemoteDNS: false, 108 | UseIPv6: false, 109 | NoTunnelIPv4: false, 110 | NoTunnelIPv6: false, 111 | SNIAddress: "", // 这应当从internal.ConnectSNI读取,但现在我们不修改其他文件 112 | KeepalivePeriod: 30 * time.Second, 113 | MTU: 1280, 114 | InitialPacketSize: 1242, 115 | ReconnectDelay: 1 * time.Second, 116 | ConnectionTimeout: 30 * time.Second, 117 | IdleTimeout: 5 * time.Minute, 118 | } 119 | } 120 | 121 | // SaveConfig writes the current application configuration to a prettified JSON file. 122 | // 123 | // Parameters: 124 | // - configPath: string - The path to save the configuration JSON file. 125 | // 126 | // Returns: 127 | // - error: An error if the configuration file cannot be written. 128 | func (*Config) SaveConfig(configPath string) error { 129 | file, err := os.Create(configPath) 130 | if err != nil { 131 | return fmt.Errorf("failed to create config file: %v", err) 132 | } 133 | defer file.Close() 134 | 135 | encoder := json.NewEncoder(file) 136 | encoder.SetIndent("", " ") 137 | if err := encoder.Encode(AppConfig); err != nil { 138 | return fmt.Errorf("failed to encode config file: %v", err) 139 | } 140 | 141 | return nil 142 | } 143 | 144 | // InitNewConfig initializes a new configuration with default values. 145 | // 146 | // Parameters: 147 | // - privateKey: string - Base64-encoded ECDSA private key. 148 | // - endpointV4: string - IPv4 address of the endpoint. 149 | // - endpointV6: string - IPv6 address of the endpoint. 150 | // - endpointPubKey: string - PEM-encoded ECDSA public key of the endpoint. 151 | // - license: string - Application license key. 152 | // - id: string - Device unique identifier. 153 | // - accessToken: string - Authentication token for API access. 154 | // - ipv4: string - Assigned IPv4 address. 155 | // - ipv6: string - Assigned IPv6 address. 156 | // - deviceName: string - Name of the device (for registration info). 157 | // 158 | // Returns: 159 | // - The newly initialized Config. 160 | func InitNewConfig( 161 | privateKey, endpointV4, endpointV6, endpointPubKey, 162 | license, id, accessToken, ipv4, ipv6, deviceName string, 163 | ) Config { 164 | return Config{ 165 | PrivateKey: privateKey, 166 | EndpointV4: endpointV4, 167 | EndpointV6: endpointV6, 168 | EndpointPubKey: endpointPubKey, 169 | License: license, 170 | ID: id, 171 | AccessToken: accessToken, 172 | IPv4: ipv4, 173 | IPv6: ipv6, 174 | Socks: GetDefaultSocksConfig(), 175 | Registration: RegistrationInfo{ 176 | DeviceName: deviceName, 177 | }, 178 | } 179 | } 180 | 181 | // GetEcPrivateKey retrieves the ECDSA private key from the stored Base64-encoded string. 182 | // 183 | // Returns: 184 | // - *ecdsa.PrivateKey: The parsed ECDSA private key. 185 | // - error: An error if decoding or parsing the private key fails. 186 | func (*Config) GetEcPrivateKey() (*ecdsa.PrivateKey, error) { 187 | privKeyB64, err := base64.StdEncoding.DecodeString(AppConfig.PrivateKey) 188 | if err != nil { 189 | return nil, fmt.Errorf("failed to decode private key: %v", err) 190 | } 191 | 192 | privKey, err := x509.ParseECPrivateKey(privKeyB64) 193 | if err != nil { 194 | return nil, fmt.Errorf("failed to parse private key: %v", err) 195 | } 196 | 197 | return privKey, nil 198 | } 199 | 200 | // GetEcEndpointPublicKey retrieves the ECDSA public key from the stored PEM-encoded string. 201 | // 202 | // Returns: 203 | // - *ecdsa.PublicKey: The parsed ECDSA public key. 204 | // - error: An error if decoding or parsing the public key fails. 205 | func (*Config) GetEcEndpointPublicKey() (*ecdsa.PublicKey, error) { 206 | endpointPubKeyB64, _ := pem.Decode([]byte(AppConfig.EndpointPubKey)) 207 | if endpointPubKeyB64 == nil { 208 | return nil, fmt.Errorf("failed to decode endpoint public key") 209 | } 210 | 211 | pubKey, err := x509.ParsePKIXPublicKey(endpointPubKeyB64.Bytes) 212 | if err != nil { 213 | return nil, fmt.Errorf("failed to parse public key: %v", err) 214 | } 215 | 216 | ecPubKey, ok := pubKey.(*ecdsa.PublicKey) 217 | if !ok { 218 | return nil, fmt.Errorf("failed to assert public key as ECDSA") 219 | } 220 | 221 | return ecPubKey, nil 222 | } 223 | -------------------------------------------------------------------------------- /internal/utils.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "crypto/rand" 7 | "crypto/x509" 8 | "encoding/base64" 9 | "encoding/hex" 10 | "errors" 11 | "math/big" 12 | "net" 13 | "strconv" 14 | "strings" 15 | "time" 16 | 17 | "github.com/quic-go/quic-go" 18 | ) 19 | 20 | // PortMapping represents a network port forwarding rule. 21 | type PortMapping struct { 22 | BindAddress string // The address to bind the local port. 23 | LocalPort int // The local port number. 24 | RemoteIP string // The remote destination IP address. 25 | RemotePort int // The remote destination port number. 26 | } 27 | 28 | // GenerateRandomAndroidSerial generates a random 8-byte Android-like device identifier 29 | // and returns it as a hexadecimal string. 30 | // 31 | // Returns: 32 | // - string: A randomly generated 16-character hexadecimal serial number. 33 | // - error: An error if random data generation fails. 34 | func GenerateRandomAndroidSerial() (string, error) { 35 | serial := make([]byte, 8) 36 | if _, err := rand.Read(serial); err != nil { 37 | return "", err 38 | } 39 | return hex.EncodeToString(serial), nil 40 | } 41 | 42 | // GenerateRandomWgPubkey generates a random 32-byte WireGuard like public key 43 | // and returns it as a base64-encoded string. 44 | // 45 | // Returns: 46 | // - string: A randomly generated WireGuard like public key in base64 format. 47 | // - error: An error if random data generation fails. 48 | func GenerateRandomWgPubkey() (string, error) { 49 | publicKey := make([]byte, 32) 50 | if _, err := rand.Read(publicKey); err != nil { 51 | return "", err 52 | } 53 | return base64.StdEncoding.EncodeToString(publicKey), nil 54 | } 55 | 56 | // TimeAsCfString formats a given time.Time into a Cloudflare-compatible string format. 57 | // 58 | // The format follows the standard: "YYYY-MM-DDTHH:MM:SS.sss-07:00". 59 | // 60 | // Parameters: 61 | // - t: time.Time to format. 62 | // 63 | // Returns: 64 | // - string: The formatted time string. 65 | func TimeAsCfString(t time.Time) string { 66 | return t.Format("2006-01-02T15:04:05.000-07:00") 67 | } 68 | 69 | // GenerateEcKeyPair generates a new ECDSA key pair using the P-256 curve. 70 | // 71 | // Returns: 72 | // - []byte: The marshalled private key in ASN.1 DER format. 73 | // - []byte: The marshalled public key in PKIX format. 74 | // - error: An error if key generation or marshalling fails. 75 | func GenerateEcKeyPair() ([]byte, []byte, error) { 76 | privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 77 | if err != nil { 78 | return nil, nil, err 79 | } 80 | 81 | marshalledPrivKey, err := x509.MarshalECPrivateKey(privKey) 82 | if err != nil { 83 | return nil, nil, err 84 | } 85 | 86 | marshalledPubKey, err := x509.MarshalPKIXPublicKey(&privKey.PublicKey) 87 | if err != nil { 88 | return nil, nil, err 89 | } 90 | 91 | return marshalledPrivKey, marshalledPubKey, nil 92 | } 93 | 94 | // GenerateCert creates a self-signed certificate using the provided ECDSA private and public keys. 95 | // 96 | // The certificate is valid for 24 hours. 97 | // 98 | // Parameters: 99 | // - privKey: *ecdsa.PrivateKey - The private key to sign the certificate. 100 | // - pubKey: *ecdsa.PublicKey - The public key to include in the certificate. 101 | // 102 | // Returns: 103 | // - [][]byte: A slice containing the certificate in DER format. 104 | // - error: An error if certificate generation fails. 105 | func GenerateCert(privKey *ecdsa.PrivateKey, pubKey *ecdsa.PublicKey) ([][]byte, error) { 106 | cert, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{ 107 | SerialNumber: big.NewInt(0), 108 | NotBefore: time.Now(), 109 | NotAfter: time.Now().Add(1 * 24 * time.Hour), 110 | }, &x509.Certificate{}, &privKey.PublicKey, privKey) 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | return [][]byte{cert}, nil 116 | } 117 | 118 | // DefaultQuicConfig returns a MASQUE compatible default QUIC configuration with specified keep-alive period and initial packet size. 119 | // 120 | // Parameters: 121 | // - keepalivePeriod: time.Duration - The duration for sending QUIC keep-alive packets. 122 | // - initialPacketSize: uint16 - The initial size of QUIC packets. (1242 seems used by the original implementation) 123 | // 124 | // Returns: 125 | // - *quic.Config: A pointer to a configured QUIC configuration object. 126 | func DefaultQuicConfig(keepalivePeriod time.Duration, initialPacketSize uint16) *quic.Config { 127 | return &quic.Config{ 128 | EnableDatagrams: true, 129 | InitialPacketSize: initialPacketSize, 130 | KeepAlivePeriod: keepalivePeriod, 131 | } 132 | } 133 | 134 | // parsePortMapping is an internal helper function that parses a port mapping string into its components. 135 | // 136 | // It handles IPv6 addresses enclosed in brackets and various format edge cases. 137 | // 138 | // Parameters: 139 | // - port: string - The port mapping string. 140 | // 141 | // Returns: 142 | // - string: The bind address. 143 | // - int: The local port. 144 | // - string: The remote hostname/IP. 145 | // - int: The remote port. 146 | // - error: An error if parsing fails. 147 | func parsePortMapping(port string) (bindAddress string, localPort int, remoteHost string, remotePort int, err error) { 148 | parts := strings.Split(port, ":") 149 | 150 | // Handle IPv6 addresses (which are enclosed in brackets) 151 | if len(parts) >= 4 && strings.HasPrefix(parts[0], "[") && strings.Contains(parts[0], "]") { 152 | bindAddress = parts[0] 153 | parts = parts[1:] // Shift parts forward 154 | } else if len(parts) == 3 { 155 | bindAddress = "localhost" // Default to localhost 156 | } else if len(parts) == 4 { 157 | bindAddress = parts[0] 158 | parts = parts[1:] // Shift forward 159 | } else { 160 | return "", 0, "", 0, errors.New("invalid port mapping format (expected format: [bind_address:]local_port:remote_host:remote_port)") 161 | } 162 | 163 | // Parse local port 164 | localPort, err = strconv.Atoi(parts[0]) 165 | if err != nil || localPort <= 0 || localPort > 65535 { 166 | return "", 0, "", 0, errors.New("invalid local port") 167 | } 168 | 169 | // Validate remote host (allow both hostnames and IPs) 170 | remoteHost = parts[1] 171 | if net.ParseIP(remoteHost) == nil && !isValidHostname(remoteHost) { 172 | return "", 0, "", 0, errors.New("invalid remote hostname/IP") 173 | } 174 | 175 | // Parse remote port 176 | remotePort, err = strconv.Atoi(parts[2]) 177 | if err != nil || remotePort <= 0 || remotePort > 65535 { 178 | return "", 0, "", 0, errors.New("invalid remote port") 179 | } 180 | 181 | // If bindAddress is an IPv6 address, remove brackets for proper binding 182 | if strings.HasPrefix(bindAddress, "[") && strings.HasSuffix(bindAddress, "]") { 183 | bindAddress = strings.Trim(bindAddress, "[]") 184 | } 185 | 186 | // Convert "localhost" or hostnames to actual addresses 187 | if bindAddress == "*" { 188 | bindAddress = "0.0.0.0" // Allow all interfaces 189 | } 190 | 191 | // Validate bind address (support both IPs and hostnames) 192 | bindAddress, err = resolveBindAddress(bindAddress) 193 | if err != nil { 194 | return "", 0, "", 0, errors.New("invalid local address: " + err.Error()) 195 | } 196 | 197 | remoteHost, err = resolveBindAddress(remoteHost) 198 | if err != nil { 199 | return "", 0, "", 0, errors.New("invalid remote address: " + err.Error()) 200 | } 201 | 202 | return bindAddress, localPort, remoteHost, remotePort, nil 203 | } 204 | 205 | // ParsePortMapping parses a port mapping string into a structured PortMapping. 206 | // 207 | // The expected format is: `[bind_address:]local_port:remote_host:remote_port`. 208 | // 209 | // Parameters: 210 | // - port: string - The port mapping string. 211 | // 212 | // Returns: 213 | // - PortMapping: A structured representation of the parsed port mapping. 214 | // - error: An error if the parsing fails. 215 | func ParsePortMapping(port string) (PortMapping, error) { 216 | bindAddress, localPort, remoteHost, remotePort, err := parsePortMapping(port) 217 | if err != nil { 218 | return PortMapping{}, err 219 | } 220 | 221 | return PortMapping{ 222 | BindAddress: bindAddress, 223 | LocalPort: localPort, 224 | RemoteIP: remoteHost, 225 | RemotePort: remotePort, 226 | }, nil 227 | } 228 | 229 | // resolveBindAddress resolves a hostname or IP to its string representation. 230 | // 231 | // Parameters: 232 | // - addr: string - The hostname or IP. 233 | // 234 | // Returns: 235 | // - string: The resolved IP address. 236 | // - error: An error if resolution fails. 237 | func resolveBindAddress(addr string) (string, error) { 238 | tcpAddr, err := net.ResolveTCPAddr("tcp", addr+":0") // Resolve the address 239 | if err != nil { 240 | return "", err 241 | } 242 | return tcpAddr.IP.String(), nil // Return resolved IP 243 | } 244 | 245 | // isValidHostname checks if a given hostname is valid. 246 | // Pretty ugly for now, needs to be refactored. 247 | // 248 | // Parameters: 249 | // - hostname: string - The hostname to validate. 250 | // 251 | // Returns: 252 | // - bool: True if valid, false otherwise. 253 | func isValidHostname(hostname string) bool { 254 | // Must contain at least one dot (.) unless it's "localhost" 255 | if hostname == "localhost" { 256 | return true 257 | } 258 | return strings.Contains(hostname, ".") 259 | } 260 | 261 | // LoginToBase64 encodes a username and password into a base64-encoded string in "username:password" format. 262 | // This is commonly used for HTTP Basic Authentication. 263 | // 264 | // Parameters: 265 | // - username: string - The username to encode. 266 | // - password: string - The password to encode. 267 | // 268 | // Returns: 269 | // - string: The base64-encoded "username:password" string. 270 | func LoginToBase64(username, password string) string { 271 | return base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) 272 | } 273 | -------------------------------------------------------------------------------- /api/tunnel.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "math/rand" 10 | "net" 11 | "sync" 12 | "sync/atomic" 13 | "time" 14 | 15 | connectip "github.com/Diniboy1123/connect-ip-go" 16 | "github.com/HynoR/uscf/internal" 17 | "golang.zx2c4.com/wireguard/tun" 18 | ) 19 | 20 | const packetBuffCap = 2048 21 | 22 | var packetBufferPool *NetBuffer 23 | 24 | // NetBuffer is a pool of byte slices with a fixed capacity. 25 | // Helps to reduce memory allocations and improve performance. 26 | // It uses a sync.Pool to manage the byte slices. 27 | // The capacity of the byte slices is set when the pool is created. 28 | type NetBuffer struct { 29 | capacity int 30 | buf sync.Pool 31 | } 32 | 33 | // Get returns a byte slice from the pool. 34 | func (n *NetBuffer) GetBuf() *[]byte { 35 | return n.buf.Get().(*[]byte) 36 | } 37 | 38 | // Put places a byte slice back into the pool. 39 | // It checks if the capacity of the byte slice matches the pool's capacity. 40 | // If it doesn't match, the byte slice is not returned to the pool. 41 | func (n *NetBuffer) PutBuf(buf *[]byte) { 42 | if cap(*buf) != n.capacity { 43 | return 44 | } 45 | n.buf.Put(buf) 46 | } 47 | 48 | // Get returns a byte slice from the pool. 49 | func (n *NetBuffer) Get() []byte { 50 | return *(n.buf.Get().(*[]byte)) 51 | } 52 | 53 | // Put places a byte slice back into the pool. 54 | // It checks if the capacity of the byte slice matches the pool's capacity. 55 | // If it doesn't match, the byte slice is not returned to the pool. 56 | func (n *NetBuffer) Put(buf []byte) { 57 | if cap(buf) != n.capacity { 58 | return 59 | } 60 | n.buf.Put(&buf) 61 | } 62 | 63 | // NewNetBuffer creates a new NetBuffer with the specified capacity. 64 | // The capacity must be greater than 0. 65 | func NewNetBuffer(capacity int) *NetBuffer { 66 | if capacity <= 0 { 67 | panic("capacity must be greater than 0") 68 | } 69 | return &NetBuffer{ 70 | capacity: capacity, 71 | buf: sync.Pool{ 72 | New: func() interface{} { 73 | b := make([]byte, capacity) 74 | return &b 75 | }, 76 | }, 77 | } 78 | } 79 | 80 | // TunnelDevice abstracts a TUN device so that we can use the same tunnel-maintenance code 81 | // regardless of the underlying implementation. 82 | type TunnelDevice interface { 83 | // ReadPacket reads a packet from the device (using the given mtu) and returns its contents. 84 | ReadPacket(buf []byte) (int, error) 85 | // WritePacket writes a packet to the device. 86 | WritePacket(pkt []byte) error 87 | } 88 | 89 | // TunnelStats 用于跟踪隧道性能指标 90 | type TunnelStats struct { 91 | PacketsIn uint64 92 | PacketsOut uint64 93 | BytesIn uint64 94 | BytesOut uint64 95 | Errors uint64 96 | HandShake uint64 97 | LastReconnect time.Time 98 | mu sync.Mutex 99 | } 100 | 101 | func (s *TunnelStats) RecordPacketIn(bytes int) { 102 | atomic.AddUint64(&s.PacketsIn, 1) 103 | atomic.AddUint64(&s.BytesIn, uint64(bytes)) 104 | } 105 | 106 | func (s *TunnelStats) RecordPacketOut(bytes int) { 107 | atomic.AddUint64(&s.PacketsOut, 1) 108 | atomic.AddUint64(&s.BytesOut, uint64(bytes)) 109 | } 110 | 111 | func (s *TunnelStats) RecordError() { 112 | atomic.AddUint64(&s.Errors, 1) 113 | } 114 | 115 | func (s *TunnelStats) RecordHandShake() { 116 | s.mu.Lock() 117 | defer s.mu.Unlock() 118 | s.HandShake++ 119 | s.LastReconnect = time.Now() 120 | } 121 | 122 | // NetstackAdapter wraps a tun.Device (e.g. from netstack) to satisfy TunnelDevice. 123 | type NetstackAdapter struct { 124 | dev tun.Device 125 | packetBufsPool sync.Pool 126 | sizesPool sync.Pool 127 | } 128 | 129 | func (n *NetstackAdapter) ReadPacket(buf []byte) (int, error) { 130 | 131 | //packetBuf := packetBufferPool.GetBuf(buf).([]byte) 132 | 133 | // 修改这一行 134 | packetBufs := n.packetBufsPool.Get().(*[][]byte) 135 | sizes := n.sizesPool.Get().(*[]int) 136 | 137 | // 确保在函数结束时将切片归还到对象池 138 | defer func() { 139 | (*packetBufs)[0] = nil // 避免内存泄漏 140 | n.packetBufsPool.Put(packetBufs) 141 | n.sizesPool.Put(sizes) 142 | }() 143 | 144 | (*packetBufs)[0] = buf 145 | (*sizes)[0] = 0 146 | 147 | _, err := n.dev.Read(*packetBufs, *sizes, 0) 148 | if err != nil { 149 | return 0, err 150 | } 151 | 152 | return (*sizes)[0], nil 153 | } 154 | 155 | func (n *NetstackAdapter) WritePacket(pkt []byte) error { 156 | // Write expects a slice of packet buffers. 157 | _, err := n.dev.Write([][]byte{pkt}, 0) 158 | return err 159 | } 160 | 161 | // NewNetstackAdapter creates a new NetstackAdapter. 162 | func NewNetstackAdapter(dev tun.Device) TunnelDevice { 163 | return &NetstackAdapter{dev: dev, 164 | packetBufsPool: sync.Pool{ 165 | New: func() interface{} { 166 | b := make([][]byte, 1) 167 | return &b 168 | }, 169 | }, 170 | sizesPool: sync.Pool{ 171 | New: func() interface{} { 172 | b := make([]int, 1) 173 | return &b 174 | }, 175 | }, 176 | } 177 | } 178 | 179 | // ConnectionConfig 包含连接配置选项 180 | type ConnectionConfig struct { 181 | TLSConfig *tls.Config 182 | KeepAlivePeriod time.Duration 183 | InitialPacketSize uint16 184 | Endpoint *net.UDPAddr 185 | MTU int 186 | MaxPacketRate float64 // 每秒最大数据包处理速率 187 | MaxBurst int // 突发处理数据包的最大数量 188 | ReconnectStrategy BackoffStrategy 189 | } 190 | 191 | // BackoffStrategy 定义重连策略接口 192 | type BackoffStrategy interface { 193 | NextDelay(attempt int) time.Duration 194 | Reset() 195 | } 196 | 197 | // ExponentialBackoff 实现指数退避重连策略 198 | type ExponentialBackoff struct { 199 | InitialDelay time.Duration 200 | MaxDelay time.Duration 201 | Factor float64 202 | } 203 | 204 | func (b *ExponentialBackoff) NextDelay(attempt int) time.Duration { 205 | if attempt <= 0 { 206 | return b.InitialDelay 207 | } 208 | 209 | // 计算指数退避延迟 210 | delay := b.InitialDelay 211 | maxDelayInFloat := float64(b.MaxDelay) / b.Factor 212 | for i := 0; i < attempt && float64(delay) < maxDelayInFloat; i++ { 213 | delay = time.Duration(float64(delay) * b.Factor) 214 | } 215 | 216 | // 确保不超过最大延迟 217 | if delay > b.MaxDelay { 218 | delay = b.MaxDelay 219 | } 220 | 221 | // 添加随机抖动以避免雷暴问题 222 | jitter := time.Duration(float64(delay) * 0.1) // 10%的抖动 223 | delay = delay - jitter + time.Duration(float64(jitter*2)*rand.Float64()) 224 | 225 | return delay 226 | } 227 | 228 | func (b *ExponentialBackoff) Reset() { 229 | // 重置状态(如果需要) 230 | } 231 | 232 | // handleForwarding 处理数据包的转发 233 | func handleForwarding(ctx context.Context, device TunnelDevice, ipConn *connectip.Conn, stats *TunnelStats) error { 234 | errChan := make(chan error, 2) 235 | ctx, cancel := context.WithCancel(ctx) 236 | defer cancel() // 确保在函数退出时取消上下文 237 | 238 | // 从设备到IP连接的转发 239 | go func() { 240 | defer cancel() // 确保在goroutine退出时取消上下文 241 | for { 242 | select { 243 | case <-ctx.Done(): 244 | return 245 | default: 246 | buf := packetBufferPool.GetBuf() 247 | 248 | n, err := device.ReadPacket(*buf) 249 | if err != nil { 250 | packetBufferPool.PutBuf(buf) 251 | errChan <- fmt.Errorf("failed to read from TUN device: %v", err) 252 | return 253 | } 254 | 255 | stats.RecordPacketOut(n) 256 | icmp, err := ipConn.WritePacket((*buf)[:n]) 257 | if err != nil { 258 | packetBufferPool.PutBuf(buf) 259 | if errors.As(err, new(*connectip.CloseError)) { 260 | errChan <- fmt.Errorf("connection closed while writing to IP connection: %v", err) 261 | return 262 | } 263 | log.Printf("Error writing to IP connection: %v, continuing...", err) 264 | continue 265 | } 266 | if cap(*buf) < 2*packetBuffCap { 267 | packetBufferPool.PutBuf(buf) 268 | } 269 | 270 | if len(icmp) > 0 { 271 | if err := device.WritePacket(icmp); err != nil { 272 | if errors.As(err, new(*connectip.CloseError)) { 273 | errChan <- fmt.Errorf("failed to write ICMP to TUN device: %v", err) 274 | return 275 | } 276 | log.Printf("Error writing to IP connection: %v, continuing...", err) 277 | continue 278 | } 279 | stats.RecordPacketIn(len(icmp)) 280 | } 281 | } 282 | } 283 | }() 284 | 285 | // 从IP连接到设备的转发 286 | go func() { 287 | defer cancel() // 确保在goroutine退出时取消上下文 288 | for { 289 | select { 290 | case <-ctx.Done(): 291 | return 292 | default: 293 | buf := packetBufferPool.GetBuf() 294 | 295 | n, err := ipConn.ReadPacket(*buf, true) 296 | if err != nil { 297 | packetBufferPool.PutBuf(buf) 298 | if errors.As(err, new(*connectip.CloseError)) { 299 | errChan <- fmt.Errorf("connection closed while reading from IP connection: %v", err) 300 | return 301 | } 302 | log.Printf("Error reading from IP connection: %v, continuing...", err) 303 | continue 304 | } 305 | 306 | stats.RecordPacketIn(n) 307 | if err := device.WritePacket((*buf)[:n]); err != nil { 308 | packetBufferPool.PutBuf(buf) 309 | errChan <- fmt.Errorf("failed to write to TUN device: %v", err) 310 | return 311 | } 312 | if cap(*buf) < 2*packetBuffCap { 313 | packetBufferPool.PutBuf(buf) 314 | } 315 | } 316 | } 317 | }() 318 | 319 | // 等待错误或上下文取消 320 | select { 321 | case err := <-errChan: 322 | return err 323 | case <-ctx.Done(): 324 | return ctx.Err() 325 | } 326 | } 327 | 328 | // monitorStats 监控统计信息 329 | func monitorStats(ctx context.Context, stats *TunnelStats) { 330 | ticker := time.NewTicker(300 * time.Second) 331 | defer ticker.Stop() 332 | 333 | for { 334 | select { 335 | case <-ctx.Done(): 336 | return 337 | case <-ticker.C: 338 | log.Printf("Tunnel stats: In: %d pkts (%d bytes), Out: %d pkts (%d bytes), Errors: %d, HandShake: %d", 339 | stats.PacketsIn, stats.BytesIn, stats.PacketsOut, stats.BytesOut, stats.Errors, stats.HandShake) 340 | } 341 | } 342 | } 343 | 344 | // handleConnection 处理单次连接 345 | func handleConnection(ctx context.Context, config ConnectionConfig, device TunnelDevice, stats *TunnelStats, reconnectAttempt int) (int, error) { 346 | log.Printf("Establishing MASQUE connection to %s:%d (attempt #%d)", 347 | config.Endpoint.IP, config.Endpoint.Port, reconnectAttempt+1) 348 | 349 | udpConn, tr, ipConn, rsp, err := ConnectTunnel( 350 | ctx, 351 | config.TLSConfig, 352 | internal.DefaultQuicConfig(config.KeepAlivePeriod, config.InitialPacketSize), 353 | internal.ConnectURI, 354 | config.Endpoint, 355 | ) 356 | 357 | if err != nil { 358 | return reconnectAttempt + 1, err 359 | } 360 | defer func() { 361 | if ipConn != nil { 362 | ipConn.Close() 363 | } 364 | if udpConn != nil { 365 | udpConn.Close() 366 | } 367 | if tr != nil { 368 | tr.Close() 369 | } 370 | }() 371 | 372 | if rsp.StatusCode != 200 { 373 | stats.RecordError() 374 | return reconnectAttempt + 1, fmt.Errorf("tunnel connection failed: %s", rsp.Status) 375 | } 376 | 377 | stats.RecordHandShake() 378 | log.Println("Connected to MASQUE server") 379 | 380 | // 创建子上下文用于转发 381 | forwardingCtx, cancel := context.WithCancel(ctx) 382 | defer cancel() 383 | 384 | // 启动监控统计 385 | go monitorStats(forwardingCtx, stats) 386 | 387 | // 处理转发 388 | 389 | if err = handleForwarding(forwardingCtx, device, ipConn, stats); err != nil { 390 | log.Printf("Forwarding error: %v", err) 391 | stats.RecordError() 392 | } 393 | 394 | return 0, err 395 | } 396 | 397 | func MaintainTunnel(ctx context.Context, config ConnectionConfig, device TunnelDevice) { 398 | stats := &TunnelStats{} 399 | reconnectAttempt := 0 400 | packetBufferPool = NewNetBuffer(config.MTU) 401 | 402 | for { 403 | select { 404 | case <-ctx.Done(): 405 | log.Println("Context canceled, stopping tunnel maintenance") 406 | return 407 | default: 408 | } 409 | 410 | reconnectAttempt, err := handleConnection(ctx, config, device, stats, reconnectAttempt) 411 | if ctx.Err() != nil { 412 | return 413 | } 414 | 415 | if err != nil { 416 | delay := config.ReconnectStrategy.NextDelay(reconnectAttempt) 417 | log.Printf("Connection error: %v. Will retry in %v", err, delay) 418 | 419 | select { 420 | case <-time.After(delay): 421 | continue 422 | case <-ctx.Done(): 423 | return 424 | } 425 | } 426 | 427 | config.ReconnectStrategy.Reset() 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /cmd/proxy.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "encoding/base64" 7 | "fmt" 8 | "log" 9 | "net" 10 | "net/netip" 11 | "os" 12 | "runtime" 13 | "strings" 14 | "time" 15 | 16 | "github.com/HynoR/uscf/models" 17 | 18 | "github.com/HynoR/uscf/api" 19 | "github.com/HynoR/uscf/config" 20 | "github.com/HynoR/uscf/internal" 21 | "github.com/spf13/cobra" 22 | "github.com/things-go/go-socks5" 23 | "golang.zx2c4.com/wireguard/tun" 24 | "golang.zx2c4.com/wireguard/tun/netstack" 25 | ) 26 | 27 | // proxyCmd 命令,结合 socks 和 register 的功能 28 | var proxyCmd = &cobra.Command{ 29 | Use: "proxy", 30 | Short: "One-command solution to run SOCKS5 proxy with auto-registration", 31 | Long: "Automatically registers if no config exists, then runs a dual-stack SOCKS5 proxy with optional authentication.", 32 | Run: runProxyCmd, 33 | } 34 | 35 | func init() { 36 | // 初始化 proxy 命令的参数 37 | 38 | // 只保留必要的注册相关参数,其他参数已转移到配置文件 39 | proxyCmd.Flags().String("locale", internal.DefaultLocale, "Locale for registration") 40 | proxyCmd.Flags().String("model", internal.DefaultModel, "Model for registration") 41 | proxyCmd.Flags().String("name", "", "Device name for registration") 42 | proxyCmd.Flags().Bool("accept-tos", true, "Automatically accept Cloudflare TOS") 43 | proxyCmd.Flags().String("jwt", "", "Team token for registration") 44 | 45 | // 添加重置SOCKS5配置的标志 46 | proxyCmd.Flags().Bool("reset-config", false, "Reset SOCKS5 configuration to default values") 47 | 48 | // 添加SOCKS5代理配置的命令行参数 49 | proxyCmd.Flags().StringP("bind-address", "b", "", "Bind address for SOCKS5 proxy (overrides config file)") 50 | proxyCmd.Flags().StringP("port", "p", "", "Port for SOCKS5 proxy (overrides config file)") 51 | proxyCmd.Flags().StringP("username", "u", "", "Username for SOCKS5 proxy authentication (overrides config file)") 52 | proxyCmd.Flags().StringP("password", "w", "", "Password for SOCKS5 proxy authentication (overrides config file)") 53 | 54 | // 添加提示,说明SOCKS配置已移至配置文件,但可通过命令行参数覆盖 55 | proxyCmd.Long += "\n\nNote: All SOCKS proxy settings are primarily managed through the config file, but can be overridden with command-line flags." 56 | 57 | // 把 proxyCmd 注册到根命令 58 | rootCmd.AddCommand(proxyCmd) 59 | } 60 | 61 | // runProxyCmd 是 proxyCmd 的执行逻辑 62 | func runProxyCmd(cmd *cobra.Command, args []string) { 63 | // 0. 获取配置文件路径 64 | configPath, err := cmd.Flags().GetString("config") 65 | if err != nil { 66 | cmd.Printf("Failed to get config path: %v\n", err) 67 | return 68 | } 69 | if configPath == "" { 70 | configPath = "config.json" 71 | } 72 | 73 | // 检查是否需要重置SOCKS5配置 74 | resetConfig, _ := cmd.Flags().GetBool("reset-config") 75 | 76 | // 1. 如有需要,进行自动注册 77 | if !config.ConfigLoaded { 78 | if err := handleRegistration(cmd, configPath); err != nil { 79 | cmd.Printf("%v\n", err) 80 | return 81 | } 82 | 83 | // 更新一些需要从内部常量获取的配置值 84 | config.AppConfig.Socks.SNIAddress = internal.ConnectSNI 85 | 86 | // 保存更新后的配置 87 | if err := config.AppConfig.SaveConfig(configPath); err != nil { 88 | log.Printf("Warning: Failed to save updated config: %v", err) 89 | } 90 | } else if resetConfig { 91 | // 如果已加载配置且指定了reset-config标志,则重置SOCKS5配置 92 | log.Println("Resetting SOCKS5 configuration to default values...") 93 | 94 | // 保存当前的SNI地址,因为它取决于内部常量 95 | sniAddress := config.AppConfig.Socks.SNIAddress 96 | 97 | // 重置为默认配置 98 | config.AppConfig.Socks = config.GetDefaultSocksConfig() 99 | 100 | // 恢复SNI地址 101 | config.AppConfig.Socks.SNIAddress = sniAddress 102 | 103 | // 保存更新后的配置 104 | if err := config.AppConfig.SaveConfig(configPath); err != nil { 105 | log.Printf("Warning: Failed to save reset config: %v", err) 106 | cmd.Printf("Failed to save reset configuration: %v\n", err) 107 | return 108 | } 109 | log.Printf("SOCKS5 configuration has been reset to default values in %s", configPath) 110 | } 111 | 112 | // 检查并应用命令行参数覆盖配置文件的值 113 | configChanged := false 114 | 115 | // 检查绑定地址 116 | if bindAddress, _ := cmd.Flags().GetString("bind-address"); bindAddress != "" { 117 | log.Printf("Overriding bind address from command line: %s", bindAddress) 118 | config.AppConfig.Socks.BindAddress = bindAddress 119 | configChanged = true 120 | } 121 | 122 | // 检查端口 123 | if port, _ := cmd.Flags().GetString("port"); port != "" { 124 | log.Printf("Overriding port from command line: %s", port) 125 | config.AppConfig.Socks.Port = port 126 | configChanged = true 127 | } 128 | 129 | // 检查用户名 130 | if username, _ := cmd.Flags().GetString("username"); username != "" { 131 | log.Printf("Overriding username from command line") 132 | config.AppConfig.Socks.Username = username 133 | configChanged = true 134 | } 135 | 136 | // 检查密码 137 | if password, _ := cmd.Flags().GetString("password"); password != "" { 138 | log.Printf("Overriding password from command line") 139 | config.AppConfig.Socks.Password = password 140 | configChanged = true 141 | } 142 | 143 | // 如果配置有变更,保存到配置文件 144 | if configChanged { 145 | log.Printf("Saving updated configuration to %s", configPath) 146 | if err := config.AppConfig.SaveConfig(configPath); err != nil { 147 | log.Printf("Warning: Failed to save updated config: %v", err) 148 | } 149 | } 150 | 151 | // 2. 启动 SOCKS5 代理 152 | if err := setupAndRunSocksProxy(cmd); err != nil { 153 | cmd.Printf("%v\n", err) 154 | return 155 | } 156 | } 157 | 158 | // handleRegistration 处理自动注册流程 159 | func handleRegistration(cmd *cobra.Command, configPath string) error { 160 | log.Println("Config not loaded. Starting automatic registration...") 161 | 162 | // 获取注册参数 163 | deviceName, _ := cmd.Flags().GetString("name") 164 | locale, _ := cmd.Flags().GetString("locale") 165 | model, _ := cmd.Flags().GetString("model") 166 | acceptTos, _ := cmd.Flags().GetBool("accept-tos") 167 | jwt, _ := cmd.Flags().GetString("jwt") 168 | 169 | log.Printf("Registering with locale %s and model %s", locale, model) 170 | 171 | // 注册账户 172 | accountData, err := api.Register(model, locale, jwt, acceptTos) 173 | if err != nil { 174 | return fmt.Errorf("Failed to register: %v", err) 175 | } 176 | 177 | // 生成密钥对 178 | privKey, pubKey, err := internal.GenerateEcKeyPair() 179 | if err != nil { 180 | return fmt.Errorf("Failed to generate key pair: %v", err) 181 | } 182 | 183 | log.Printf("Enrolling device key...") 184 | 185 | // 注册设备密钥 186 | updatedAccountData, apiErr, err := api.EnrollKey(accountData, pubKey, deviceName) 187 | if err != nil { 188 | if apiErr != nil { 189 | return fmt.Errorf("Failed to enroll key: %v (API errors: %s)", err, apiErr.ErrorsAsString("; ")) 190 | } 191 | return fmt.Errorf("Failed to enroll key: %v", err) 192 | } 193 | 194 | log.Printf("Registration successful. Saving config...") 195 | 196 | // 保存配置,使用InitNewConfig创建带有默认值的配置 197 | config.AppConfig = config.InitNewConfig( 198 | base64.StdEncoding.EncodeToString(privKey), 199 | // TODO: proper endpoint parsing in utils 200 | // strip :0 201 | updatedAccountData.Config.Peers[0].Endpoint.V4[:len(updatedAccountData.Config.Peers[0].Endpoint.V4)-2], 202 | // strip [ from beginning and ]:0 from end 203 | updatedAccountData.Config.Peers[0].Endpoint.V6[1:len(updatedAccountData.Config.Peers[0].Endpoint.V6)-3], 204 | updatedAccountData.Config.Peers[0].PublicKey, 205 | updatedAccountData.Account.License, 206 | updatedAccountData.ID, 207 | accountData.Token, 208 | updatedAccountData.Config.Interface.Addresses.V4, 209 | updatedAccountData.Config.Interface.Addresses.V6, 210 | deviceName, 211 | ) 212 | 213 | err = config.AppConfig.SaveConfig(configPath) 214 | if err != nil { 215 | return fmt.Errorf("Failed to save config: %v", err) 216 | } 217 | 218 | log.Printf("Config saved to %s", configPath) 219 | 220 | // 标记配置已加载 221 | config.ConfigLoaded = true 222 | return nil 223 | } 224 | 225 | // setupAndRunSocksProxy 设置并运行SOCKS5代理 226 | func setupAndRunSocksProxy(cmd *cobra.Command) error { 227 | log.Println("Starting SOCKS5 proxy...") 228 | 229 | // 设置最大并发处理能力 230 | runtime.GOMAXPROCS(runtime.NumCPU()) 231 | 232 | // 准备TLS配置 233 | tlsConfig, err := prepareTlsConfig(cmd) 234 | if err != nil { 235 | return err 236 | } 237 | 238 | // 准备网络配置 239 | endpoint, localAddresses, dnsAddrs, err := prepareNetworkConfig(cmd) 240 | if err != nil { 241 | return err 242 | } 243 | 244 | // 获取超时设置 245 | connectionTimeout, idleTimeout := getTimeoutSettings(cmd) 246 | 247 | // 创建TUN设备 248 | tunDev, tunNet, err := createTunDevice(localAddresses, dnsAddrs, cmd) 249 | if err != nil { 250 | return err 251 | } 252 | defer tunDev.Close() 253 | 254 | // 配置连接并启动隧道 255 | startTunnel(cmd, tlsConfig, endpoint, tunDev) 256 | 257 | // 创建并启动SOCKS服务器 258 | return runSocksServer(cmd, tunNet, connectionTimeout, idleTimeout) 259 | } 260 | 261 | // prepareTlsConfig 准备TLS配置 262 | func prepareTlsConfig(cmd *cobra.Command) (*tls.Config, error) { 263 | // 从配置中获取SNI地址 264 | sni := config.AppConfig.Socks.SNIAddress 265 | 266 | privKey, err := config.AppConfig.GetEcPrivateKey() 267 | if err != nil { 268 | return nil, fmt.Errorf("Failed to get private key: %v", err) 269 | } 270 | peerPubKey, err := config.AppConfig.GetEcEndpointPublicKey() 271 | if err != nil { 272 | return nil, fmt.Errorf("Failed to get public key: %v", err) 273 | } 274 | 275 | cert, err := internal.GenerateCert(privKey, &privKey.PublicKey) 276 | if err != nil { 277 | return nil, fmt.Errorf("Failed to generate cert: %v", err) 278 | } 279 | 280 | tlsConfig, err := api.PrepareTlsConfig(privKey, peerPubKey, cert, sni) 281 | if err != nil { 282 | return nil, fmt.Errorf("Failed to prepare TLS config: %v", err) 283 | } 284 | return tlsConfig, nil 285 | } 286 | 287 | // prepareNetworkConfig 准备网络配置 288 | func prepareNetworkConfig(cmd *cobra.Command) (*net.UDPAddr, []netip.Addr, []netip.Addr, error) { 289 | // 从配置文件获取连接端口 290 | connectPort := config.AppConfig.Socks.ConnectPort 291 | 292 | // 确定使用IPv4还是IPv6端点 293 | var endpoint *net.UDPAddr 294 | if !config.AppConfig.Socks.UseIPv6 { 295 | endpoint = &net.UDPAddr{ 296 | IP: net.ParseIP(config.AppConfig.EndpointV4), 297 | Port: connectPort, 298 | } 299 | } else { 300 | endpoint = &net.UDPAddr{ 301 | IP: net.ParseIP(config.AppConfig.EndpointV6), 302 | Port: connectPort, 303 | } 304 | } 305 | 306 | // 隧道内IP设置 307 | var localAddresses []netip.Addr 308 | if !config.AppConfig.Socks.NoTunnelIPv4 { 309 | v4, err := netip.ParseAddr(config.AppConfig.IPv4) 310 | if err != nil { 311 | return nil, nil, nil, fmt.Errorf("Failed to parse IPv4 address: %v", err) 312 | } 313 | localAddresses = append(localAddresses, v4) 314 | } 315 | if !config.AppConfig.Socks.NoTunnelIPv6 { 316 | v6, err := netip.ParseAddr(config.AppConfig.IPv6) 317 | if err != nil { 318 | return nil, nil, nil, fmt.Errorf("Failed to parse IPv6 address: %v", err) 319 | } 320 | localAddresses = append(localAddresses, v6) 321 | } 322 | 323 | // DNS设置 324 | var dnsAddrs []netip.Addr 325 | for _, dns := range config.AppConfig.Socks.DNS { 326 | addr, err := netip.ParseAddr(dns) 327 | if err != nil { 328 | return nil, nil, nil, fmt.Errorf("Failed to parse DNS server: %v", err) 329 | } 330 | dnsAddrs = append(dnsAddrs, addr) 331 | } 332 | 333 | return endpoint, localAddresses, dnsAddrs, nil 334 | } 335 | 336 | // getTimeoutSettings 获取超时设置 337 | func getTimeoutSettings(cmd *cobra.Command) (time.Duration, time.Duration) { 338 | // 直接从配置文件中读取超时设置 339 | connectionTimeout := config.AppConfig.Socks.ConnectionTimeout 340 | idleTimeout := config.AppConfig.Socks.IdleTimeout 341 | 342 | // 确保设置了默认值 343 | if connectionTimeout == 0 { 344 | connectionTimeout = 30 * time.Second 345 | } 346 | 347 | if idleTimeout == 0 { 348 | idleTimeout = 5 * time.Minute 349 | } 350 | 351 | return connectionTimeout, idleTimeout 352 | } 353 | 354 | // createTunDevice 创建TUN设备 355 | func createTunDevice(localAddresses, dnsAddrs []netip.Addr, cmd *cobra.Command) (tun.Device, *netstack.Net, error) { 356 | // 从配置中获取MTU 357 | mtu := config.AppConfig.Socks.MTU 358 | if mtu != 1280 { 359 | log.Println("Warning: MTU is not the default 1280. This is not supported. Packet loss and other issues may occur.") 360 | } 361 | 362 | tunDev, tunNet, err := netstack.CreateNetTUN(localAddresses, dnsAddrs, mtu) 363 | if err != nil { 364 | return nil, nil, fmt.Errorf("Failed to create virtual TUN device: %v", err) 365 | } 366 | return tunDev, tunNet, nil 367 | } 368 | 369 | // startTunnel 配置并启动隧道连接 370 | func startTunnel(cmd *cobra.Command, tlsConfig *tls.Config, endpoint *net.UDPAddr, tunDev tun.Device) { 371 | // 从配置文件读取隧道参数 372 | keepalivePeriod := config.AppConfig.Socks.KeepalivePeriod 373 | initialPacketSize := config.AppConfig.Socks.InitialPacketSize 374 | mtu := config.AppConfig.Socks.MTU 375 | reconnectDelay := config.AppConfig.Socks.ReconnectDelay 376 | 377 | configTunnel := api.ConnectionConfig{ 378 | TLSConfig: tlsConfig, 379 | KeepAlivePeriod: keepalivePeriod, 380 | InitialPacketSize: initialPacketSize, 381 | Endpoint: endpoint, 382 | MTU: mtu, 383 | MaxPacketRate: 8192, 384 | MaxBurst: 1024, 385 | ReconnectStrategy: &api.ExponentialBackoff{ 386 | InitialDelay: reconnectDelay, 387 | MaxDelay: 5 * time.Minute, 388 | Factor: 2.0, 389 | }, 390 | } 391 | 392 | go api.MaintainTunnel( 393 | context.Background(), 394 | configTunnel, 395 | api.NewNetstackAdapter(tunDev), 396 | ) 397 | } 398 | 399 | // runSocksServer 创建并运行SOCKS5服务器 400 | func runSocksServer(cmd *cobra.Command, tunNet *netstack.Net, connectionTimeout, idleTimeout time.Duration) error { 401 | // 从配置中获取网络参数 402 | bindAddress := config.AppConfig.Socks.BindAddress 403 | port := config.AppConfig.Socks.Port 404 | 405 | // 根据配置选择DNS解析器 406 | var resolver socks5.NameResolver 407 | if config.AppConfig.Socks.RemoteDNS { 408 | // 使用TunnelDNSResolver,让DNS通过TUN隧道 409 | log.Println("Using remote DNS resolver through TUN tunnel") 410 | 411 | // 解析DNS服务器地址 412 | var dnsAddrs []netip.Addr 413 | for _, dns := range config.AppConfig.Socks.DNS { 414 | addr, err := netip.ParseAddr(dns) 415 | if err != nil { 416 | return fmt.Errorf("Failed to parse DNS server %s: %v", dns, err) 417 | } 418 | dnsAddrs = append(dnsAddrs, addr) 419 | } 420 | 421 | resolver = api.NewTunnelDNSResolver(tunNet, dnsAddrs, config.AppConfig.Socks.DNSTimeout) 422 | } else { 423 | // 使用本地DNS解析器 424 | log.Println("Using local DNS resolver") 425 | dnsTimeout := config.AppConfig.Socks.DNSTimeout 426 | dnsTimeoutSeconds := int(dnsTimeout.Seconds()) 427 | if len(config.AppConfig.Socks.DNS) > 0 { 428 | // 检查 ip 后有没有端口 如果没有 加上:53 429 | ip := config.AppConfig.Socks.DNS[0] 430 | if !strings.Contains(ip, ":") { 431 | ip = ip + ":53" 432 | } 433 | resolver = api.NewCachingDNSResolver( 434 | ip, 435 | dnsTimeoutSeconds, 436 | ) 437 | } else { 438 | resolver = api.NewCachingDNSResolver( 439 | "8.8.8.8:53", 440 | dnsTimeoutSeconds, 441 | ) 442 | } 443 | 444 | } 445 | 446 | // 添加超时设置的拨号函数 447 | dialFunc := func(ctx context.Context, network, addr string) (net.Conn, error) { 448 | dialCtx, cancel := context.WithTimeout(ctx, connectionTimeout) 449 | defer cancel() 450 | 451 | conn, err := tunNet.DialContext(dialCtx, network, addr) 452 | if err != nil { 453 | return nil, err 454 | } 455 | 456 | return &models.TimeoutConn{ 457 | Conn: conn, 458 | IdleTimeout: idleTimeout, 459 | }, nil 460 | } 461 | 462 | // 从配置中获取身份验证设置 463 | username := config.AppConfig.Socks.Username 464 | password := config.AppConfig.Socks.Password 465 | 466 | // 创建SOCKS5服务器 467 | server := createSocksServer(username, password, dialFunc, resolver) 468 | 469 | // 启动监听 470 | log.Printf("SOCKS proxy listening on %s:%s with timeouts (connect: %s, idle: %s)", 471 | bindAddress, port, connectionTimeout, idleTimeout) 472 | 473 | listener, err := net.Listen("tcp", net.JoinHostPort(bindAddress, port)) 474 | if err != nil { 475 | return fmt.Errorf("Failed to start SOCKS proxy: %v", err) 476 | } 477 | 478 | for { 479 | conn, err := listener.Accept() 480 | if err != nil { 481 | log.Printf("Failed to accept connection: %v\n", err) 482 | continue 483 | } 484 | 485 | timeoutConn := &models.TimeoutConn{ 486 | Conn: conn, 487 | IdleTimeout: idleTimeout, 488 | } 489 | 490 | go server.ServeConn(timeoutConn) 491 | } 492 | } 493 | 494 | // createSocksServer 创建SOCKS5服务器 495 | func createSocksServer(username, password string, dialFunc func(ctx context.Context, network, addr string) (net.Conn, error), resolver socks5.NameResolver) *socks5.Server { 496 | buf := api.NewNetBuffer(32 * 1024) // 32KB buffer 497 | if buf == nil { 498 | log.Println("Failed to create buffer") 499 | return nil 500 | } 501 | 502 | if username == "" || password == "" { 503 | return socks5.NewServer( 504 | socks5.WithLogger(socks5.NewLogger(log.New(os.Stdout, "socks5: ", log.LstdFlags))), 505 | socks5.WithDial(dialFunc), 506 | socks5.WithResolver(resolver), 507 | socks5.WithBufferPool(buf), 508 | ) 509 | } else { 510 | 511 | return socks5.NewServer( 512 | socks5.WithLogger(socks5.NewLogger(log.New(os.Stdout, "socks5: ", log.LstdFlags))), 513 | socks5.WithDial(dialFunc), 514 | socks5.WithResolver(resolver), 515 | socks5.WithAuthMethods([]socks5.Authenticator{ 516 | socks5.UserPassAuthenticator{ 517 | Credentials: socks5.StaticCredentials{ 518 | username: password, 519 | }, 520 | }}), 521 | socks5.WithBufferPool(buf), 522 | ) 523 | } 524 | } 525 | --------------------------------------------------------------------------------