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
+}