From 6f35e356bc6fcf1c3b60cee3d13c591992cc78fb Mon Sep 17 00:00:00 2001 From: notsure2 Date: Mon, 5 Jan 2026 16:49:02 +0200 Subject: [PATCH 1/3] Update server random algorithm to use proton vpn latest + add random human strategy. --- README.md | 4 +- internal/client/TLS.go | 53 ++--- .../server_name_utils/consistent_hash.go | 98 ++++++++ .../server_name_utils/server_name_utils.go | 223 ++++++++++++++++++ 4 files changed, 340 insertions(+), 38 deletions(-) create mode 100644 internal/client/server_name_utils/consistent_hash.go create mode 100644 internal/client/server_name_utils/server_name_utils.go diff --git a/README.md b/README.md index e4c0511..9960eab 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/internal/client/TLS.go b/internal/client/TLS.go index 178f8fb..32bf8b9 100644 --- a/internal/client/TLS.go +++ b/internal/client/TLS.go @@ -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 diff --git a/internal/client/server_name_utils/consistent_hash.go b/internal/client/server_name_utils/consistent_hash.go new file mode 100644 index 0000000..c4d4e15 --- /dev/null +++ b/internal/client/server_name_utils/consistent_hash.go @@ -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 . + */ + +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 + } +} diff --git a/internal/client/server_name_utils/server_name_utils.go b/internal/client/server_name_utils/server_name_utils.go new file mode 100644 index 0000000..e9aac86 --- /dev/null +++ b/internal/client/server_name_utils/server_name_utils.go @@ -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 . + */ + +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 +} From 3690ae037b9b3841c0cea6102d3d3d417744acc3 Mon Sep 17 00:00:00 2001 From: notsure2 Date: Mon, 5 Jan 2026 18:26:39 +0200 Subject: [PATCH 2/3] Missing dot. --- internal/client/server_name_utils/server_name_utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/client/server_name_utils/server_name_utils.go b/internal/client/server_name_utils/server_name_utils.go index e9aac86..1574f2b 100644 --- a/internal/client/server_name_utils/server_name_utils.go +++ b/internal/client/server_name_utils/server_name_utils.go @@ -206,7 +206,7 @@ func randomHumanReadableServerName() string { for j := 0; j < (rand.Intn(2) + 2); j++ { name += randItem(syllables) } - domain = name + randItem(topLevelDomains) + domain = name + "." + randItem(topLevelDomains) case 2: // API/CDN Masking (e.g., "v2-node-42.static-cache.net") vNum := rand.Intn(4) + 1 From 8ddcf22780bd2141f4712b011b20412ce57510b5 Mon Sep 17 00:00:00 2001 From: notsure2 Date: Mon, 5 Jan 2026 18:36:36 +0200 Subject: [PATCH 3/3] Another missing dot. --- internal/client/server_name_utils/server_name_utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/client/server_name_utils/server_name_utils.go b/internal/client/server_name_utils/server_name_utils.go index 1574f2b..b85c57d 100644 --- a/internal/client/server_name_utils/server_name_utils.go +++ b/internal/client/server_name_utils/server_name_utils.go @@ -198,7 +198,7 @@ func randomHumanReadableServerName() string { switch style { case 0: // Dictionary-based (e.g., "secure-bridge.com") - domain = randItem(prefixes) + "-" + randItem(suffixes) + randItem(topLevelDomains) + domain = randItem(prefixes) + "-" + randItem(suffixes) + "." + randItem(topLevelDomains) case 1: // Phonetic/Brandable (e.g., "verantix.net") // Combine 2 to 3 syllables