Update server random algorithm to use proton vpn latest + add random human strategy.

This commit is contained in:
notsure2 2026-01-05 16:49:02 +02:00
parent c3d5470ef7
commit 6f35e356bc
4 changed files with 340 additions and 38 deletions

View File

@ -137,7 +137,9 @@ random-like. **You may only leave it as `plain` if you are certain that your und
encryption and authentication (via AEAD or similar techniques).**
`ServerName` is the domain you want to make your ISP or firewall _think_ you are visiting. Ideally it should
match `RedirAddr` in the server's configuration, a major site the censor allows, but it doesn't have to. Use `random` to randomize the server name for every connection made.
match `RedirAddr` in the server's configuration, a major site the censor allows, but it doesn't have to.
Use `random` to randomize the server name for every connection made. Use `randomTop` to randomize the server name from
a list of top accessed domains. Use `randomHuman` to use a randomized but human-readable server name.
`AlternativeNames` is an array used alongside `ServerName` to shuffle between different ServerNames for every new
connection. **This may conflict with `CDN` Transport mode** if the CDN provider prohibits domain fronting and rejects

View File

@ -1,11 +1,14 @@
package client
import (
"net"
"strconv"
"strings"
"github.com/cbeuw/Cloak/internal/client/server_name_utils"
"github.com/cbeuw/Cloak/internal/common"
utls "github.com/refraction-networking/utls"
log "github.com/sirupsen/logrus"
"net"
"strings"
)
const appDataMaxLength = 16401
@ -30,40 +33,6 @@ type DirectTLS struct {
browser browser
}
var topLevelDomains = []string{"com", "net", "org", "it", "fr", "me", "ru", "cn", "es", "tr", "top", "xyz", "info"}
func randomServerName() string {
/*
Copyright: Proton AG
https://github.com/ProtonVPN/wireguard-go/commit/bcf344b39b213c1f32147851af0d2a8da9266883
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
charNum := int('z') - int('a') + 1
size := 3 + common.RandInt(10)
name := make([]byte, size)
for i := range name {
name[i] = byte(int('a') + common.RandInt(charNum))
}
return string(name) + "." + common.RandItem(topLevelDomains)
}
func buildClientHello(browser browser, fields clientHelloFields) ([]byte, error) {
// We don't use utls to handle connections (as it'll attempt a real TLS negotiation)
// We only want it to build the ClientHello locally
@ -79,6 +48,10 @@ func buildClientHello(browser browser, fields clientHelloFields) ([]byte, error)
}
uclient := utls.UClient(&fakeConn, &utls.Config{ServerName: fields.serverName}, helloID)
defer func(uclient *utls.UConn) {
_ = uclient.Close()
}(uclient)
if err := uclient.BuildHandshakeState(); err != nil {
return []byte{}, err
}
@ -123,8 +96,14 @@ func (tls *DirectTLS) Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey
serverName: authInfo.MockDomain,
}
randomAddrInput := rawConn.RemoteAddr().String() + "-" + strconv.Itoa(int(authInfo.SessionId))
if strings.EqualFold(fields.serverName, "random") {
fields.serverName = randomServerName()
fields.serverName = server_name_utils.ServerNameFor(server_name_utils.ServerNameRandom, randomAddrInput)
} else if strings.EqualFold(fields.serverName, "randomTop") {
fields.serverName = server_name_utils.ServerNameFor(server_name_utils.ServerNameTop, randomAddrInput)
} else if strings.EqualFold(fields.serverName, "randomHuman") {
fields.serverName = server_name_utils.ServerNameFor(server_name_utils.ServerNameHuman, randomAddrInput)
}
var ch []byte

View File

@ -0,0 +1,98 @@
/*
* Copyright (c) 2025. Proton AG
*
* This file is part of ProtonVPN.
*
* ProtonVPN is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonVPN is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonVPN. If not, see <https://www.gnu.org/licenses/>.
*/
package server_name_utils
import (
"hash/crc32"
"math"
"sort"
)
// Set of utilities to implement consistent hashing where map one set of strings (keys) to another (values).
// Set of values will change over time but mapping will remain largely stable.
// Usage:
// hashedValues := sortValuesByHash(values, crc32Hash)
// value := findClosestValue(key, hashedValues, crc32Hash)
// when set of values changes (e.g. new one is added), only mappings that will change are for keys for which
// new value is the closest one (its uint32 hash is closest to key's hash).
type HashedValue struct {
value string
hash uint32
}
// Picks value out of sortedValuesWithHashes which hash is closest to hash of value. For distance
// calculation, uint32 is forming a ring where 0 is next to math.MaxUint32. Closer of clockwise and
// counter-clockwise distance is picked.
func findClosestValue(key string, sortedValuesWithHashes []HashedValue, hashFun func(string) uint32) string {
n := len(sortedValuesWithHashes)
if n == 0 {
return ""
} else if n == 1 {
return sortedValuesWithHashes[0].value
}
keyHash := hashFun(key)
i := sort.Search(n, func(i int) bool {
return sortedValuesWithHashes[i].hash >= keyHash
})
if i <= 0 || i >= n {
// If it's smaller than first or larger than last, return closest
// between first and last
return closerValue(keyHash, sortedValuesWithHashes[0], sortedValuesWithHashes[n-1])
} else {
return closerValue(keyHash, sortedValuesWithHashes[i-1], sortedValuesWithHashes[i])
}
}
func sortValuesByHash(values []string, hashFun func(string) uint32) []HashedValue {
hashedValues := make([]HashedValue, len(values))
for i, domain := range values {
hashedValues[i] = HashedValue{domain, hashFun(domain)}
}
sort.Slice(hashedValues, func(i, j int) bool {
return hashedValues[i].hash < hashedValues[j].hash
})
return hashedValues
}
func crc32Hash(s string) uint32 {
return crc32.ChecksumIEEE([]byte(s))
}
func ringDistance(a, b uint32) int64 {
var fa = int64(a)
var fb = int64(b)
var large = max(fa, fb)
var small = min(fa, fb)
// Take smaller of clockwise and counter-clockwise distance
return min(large-small, small-large+math.MaxUint32)
}
func closerValue(hash uint32, a HashedValue, b HashedValue) string {
var da = ringDistance(hash, a.hash)
var db = ringDistance(hash, b.hash)
if da < db {
return a.value
} else {
return b.value
}
}

View File

@ -0,0 +1,223 @@
/*
* Copyright (c) 2025. Proton AG
*
* This file is part of ProtonVPN and modified for Cloak.
*
* ProtonVPN is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonVPN is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonVPN. If not, see <https://www.gnu.org/licenses/>.
*/
package server_name_utils
import (
cryptoRand "crypto/rand"
"fmt"
"math/big"
"math/rand"
)
type ServerNameStrategy int
const (
ServerNameRandom ServerNameStrategy = iota
ServerNameTop
ServerNameHuman = 2
)
var topLevelDomains = []string{"com", "net", "org", "it", "fr", "me", "ru", "cn", "es", "tr", "top", "xyz", "info"}
var domains = []string{
"accounts.google.com",
"activity.windows.com",
"analytics.apis.mcafee.com",
"android.apis.google.com",
"android.googleapis.com",
"api.account.samsung.com",
"api.accounts.firefox.com",
"api.accuweather.com",
"api.amazon.com",
"api.browser.yandex.net",
"api.ipify.org",
"api.onedrive.com",
"api.reasonsecurity.com",
"api.samsungcloud.com",
"api.sec.intl.miui.com",
"api.vk.com",
"api.weather.com",
"app-site-association.cdn-apple.com",
"apps.mzstatic.com",
"assets.msn.com",
"backup.googleapis.com",
"brave-core-ext.s3.brave.com",
"caldav.calendar.yahoo.com",
"cc-api-data.adobe.io",
"cdn.ampproject.org",
"cdn.cookielaw.org",
"client.wns.windows.com",
"cloudflare.com",
"cloudflare-dns.com",
"cloudflare-ech.com",
"config.extension.grammarly.com",
"connectivitycheck.android.com",
"connectivitycheck.gstatic.com",
"courier.push.apple.com",
"crl.globalsign.com",
"dc1-file.ksn.kaspersky-labs.com",
"dl.google.com",
"dns.google",
"dns.quad9.net",
"doh.cleanbrowsing.org",
"doh.dns.apple.com",
"doh.opendns.com",
"doh.pub",
"ds.kaspersky.com",
"ecs.office.com",
"edge.microsoft.com",
"events.gfe.nvidia.com",
"excess.duolingo.com",
"firefox.settings.services.mozilla.com",
"fonts.googleapis.com",
"fonts.gstatic.com",
"gateway-asset.icloud-content.com",
"gateway.icloud.com",
"gdmf.apple.com",
"github.com",
"go.microsoft.com",
"go-updater.brave.com",
"graph.microsoft.com",
"gs-loc.apple.com",
"gtglobal.intl.miui.com",
"hcaptcha.com",
"imap.gmail.com",
"imap-mail.outlook.com",
"imap.mail.yahoo.com",
"in.appcenter.ms",
"ipmcdn.avast.com",
"itunes.apple.com",
"loc.map.baidu.com",
"login.live.com",
"login.microsoftonline.com",
"m.media-amazon.com",
"mobile.events.data.microsoft.com",
"mozilla.cloudflare-dns.com",
"mtalk.google.com",
"nimbus.bitdefender.net",
"ocsp2.apple.com",
"outlook.office365.com",
"play-fe.googleapis.com",
"play.googleapis.com",
"play.samsungcloud.com",
"raw.githubusercontent.com",
"s3.amazonaws.com",
"safebrowsing.googleapis.com",
"s.alicdn.com",
"self.events.data.microsoft.com",
"settings-win.data.microsoft.com",
"setup.icloud.com",
"sirius.mwbsys.com",
"spoc.norton.com",
"ssl.gstatic.com",
"translate.goo",
"unpkg.com",
"update.googleapis.com",
"weatherapi.intl.xiaomi.com",
"weatherkit.apple.com",
"westus-0.in.applicationinsights.azure.com",
"www.googleapis.com",
"www.gstatic.com",
"www.msftconnecttest.com",
"www.msftncsi.com",
"www.ntppool.org",
"www.pool.ntp.org",
"www.recaptcha.net",
}
// Data pools for linguistic generation
var prefixes = []string{
"cloud",
"global", "fast", "secure", "smart", "net", "data", "prime", "alpha", "edge"}
var suffixes = []string{"logic", "stream", "flow", "point", "nexus", "bridge", "lab", "hub", "tech", "base"}
var syllables = []string{"ver", "ant", "ix", "cor", "mon", "tel", "al", "is", "ex", "ta", "vi", "ro"}
var apiRoots = []string{"assets-delivery", "static-cache", "api-gateway", "edge-compute", "cdn-services"}
var domainsSortedByHashes = sortValuesByHash(domains, crc32Hash)
func ServerNameFor(strategy ServerNameStrategy, addr string) string {
switch strategy {
case ServerNameTop:
return serverNameForAddr(addr)
case ServerNameRandom:
return randomServerName()
case ServerNameHuman:
return randomHumanReadableServerName()
default:
return randomServerName()
}
}
func serverNameForAddr(addr string) string {
return findClosestValue(addr, domainsSortedByHashes, crc32Hash)
}
func randomServerName() string {
charNum := int('z') - int('a') + 1
size := 3 + randInt(10)
name := make([]byte, size)
for i := range name {
name[i] = byte(int('a') + randInt(charNum))
}
return string(name) + "." + randItem(topLevelDomains)
}
func randItem(list []string) string {
return list[randInt(len(list))]
}
func randInt(n int) int {
size, err := cryptoRand.Int(cryptoRand.Reader, big.NewInt(int64(n)))
if err == nil {
return int(size.Int64())
}
return rand.Intn(n)
}
func randomHumanReadableServerName() string {
// Randomly choose a generation style (0: Dictionary, 1: Phonetic, 2: API Mask)
style := rand.Intn(3)
var domain string
switch style {
case 0: // Dictionary-based (e.g., "secure-bridge.com")
domain = randItem(prefixes) + "-" + randItem(suffixes) + randItem(topLevelDomains)
case 1: // Phonetic/Brandable (e.g., "verantix.net")
// Combine 2 to 3 syllables
name := ""
for j := 0; j < (rand.Intn(2) + 2); j++ {
name += randItem(syllables)
}
domain = name + randItem(topLevelDomains)
case 2: // API/CDN Masking (e.g., "v2-node-42.static-cache.net")
vNum := rand.Intn(4) + 1
nodeNum := rand.Intn(90) + 10
root := randItem(apiRoots)
tld := ".net"
if rand.Intn(2) == 0 {
tld = ".com"
}
domain = fmt.Sprintf("v%d-node-%d.%s%s", vNum, nodeNum, root, tld)
}
return domain
}