Go net/http TLSフィンガープリントの修正:なぜGoクライアントがブロックされるのか
Goでスクレイパーや自動化クライアントを構築した経験がある方なら、ブラウザでは問題なく開けるページにnet/httpでアクセスすると403や1,023が返される現象に遭遇したことがあるでしょう。原因の多くは、コンテンツではなくTLSフィンガープリントにあります。Goの標準ライブラリcrypto/tlsが生成するClientHelloメッセージは、ChromeやSafariなどの一般的なブラウザと構造が大きく異なり、CloudflareやAkamaiのWAFはこれをJA3/JA4ハッシュで瞬時に識別します。本記事では、Go net/http TLSフィンガープリントの修正を実装レベルで解説し、uTLSによる偽装とProxyHat住宅プロキシの組み合わせで検知を回避する方法を示します。
ここで扱うのは正当な自動化、セキュリティ研究、認可済みペンテストの文脈です。対象サイトの利用規約とrobots.txtを遵守し、適法な範囲でアクセスすることが前提です。
技術的背景:GoのClientHelloがChromeと異なる理由
TLSハンドシェイクの最初のメッセージであるClientHelloには、クライアントがサポートする暗号スイート、拡張機能、楕円曲線グループ、署名アルゴリズムなどが含まれます。WAFはこのClientHelloのバイト列をハッシュ化し、JA3(および後述するJA4)という指紋を生成します。Goのcrypto/tlsは以下の特徴を持ちます:
- 固定の暗号スイート順序:Goは独自の優先順序で暗号スイートを並べるため、Chromeの順序と一致しません。
- GREASE値の欠落:Chromeは将来の拡張性のためにランダムなGREASE(RFC draft)値を挿入しますが、Goは挿入しません。これが最も顕著な差異の一つです。
- 拡張機能の過不足:Chromeが送る
encrypted_client_hello、delegated_credentials、application_settingsなどの拡張がGoには存在しない一方、Goが送るrenegotiation_infoなどはChromeにはありません。 - key_shareの曲線セット:ChromeはX25519とP-256のkey_shareを同時に送りますが、Goのデフォルト挙動は異なります。
実際のJA3/JA4を確認する
自分のGoクライアントがどのJA3/JA4を提示しているかを確認するには、tls.peet.ws/api/allにリクエストを送ります。以下は素のnet/httpでの例です:
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
resp, err := http.Get("https://tls.peet.ws/api/all")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}レスポンスのJSONにja3およびja4フィールドが含まれます。Go 1.22の典型的なJA3ハッシュは773,4865-4866-4867-49195-49199-...,-のような文字列になり、ChromeのJA3とは完全に異なります。Chrome 120のJA3はGREASE値を含むため、ハッシュ値自体がGoとは別物です。WAFはこの差をミリ秒単位で判定します。
ClientHelloシグナルの詳細比較
GoとChromeのClientHelloを具体的に比較すると、以下のシグナルで決定的な差が生まれます:
| シグナル | Go 1.22 (net/http) | Chrome 120+ |
|---|---|---|
| 暗号スイート先頭 | TLS_AES_128_GCM_SHA256 | GREASE → TLS_AES_128_GCM_SHA256 |
| supported_groups | X25519, secp256r1, secp384r1 | GREASE, X25519, secp256r1, secp384r1 |
| signature_algorithms | 固定リスト(ecdsa_secp256r1_sha256等) | GREASE入り、より多くのアルゴリズム |
| ALPN | h2, http/1.1 | h2, http/1.1(同一だが拡張順序が異なる) |
| key_share | X25519のみ(デフォルト) | X25519 + secp256r1の両方 |
| GREASE拡張 | なし | 複数のランダムGREASE値 |
| encrypted_client_hello | サポートせず | デフォルトで送信 |
この表が示す通り、GoのTLSフィンガープリントがChromeと異なる理由は単一の要因ではありません。暗号スイートの順序、GREASEの有無、拡張機能の構成、key_shareの曲線セットが複合的に作用し、結果としてJA3/JA4ハッシュが完全に別物になります。
WAFはIP評価だけでなくTLS指紋も検査するため、たとえクリーンなIPを使っていてもGoのデフォルトClientHelloでは弾かれます。フィンガープリント偽装とIP評価の両方が必要です。
refraction-networking/utlsによる修正実装
refraction-networking/utlsは、Goのcrypto/tlsをフォークし、ClientHelloを任意のブラウザプロファイルに偽装できるライブラリです。最も一般的なアプローチはutls.UClientにutls.HelloChrome_Autoを指定し、それをhttp.TransportのDialTLSContextに接続することです。
package main
import (
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"time"
utls "github.com/refraction-networking/utls"
)
func newUTLSTransport() *http.Transport {
return &http.Transport{
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
host, _, _ := net.SplitHostPort(addr)
rawConn, err := (&net.Dialer{
Timeout: 15 * time.Second,
}).DialContext(ctx, network, addr)
if err != nil {
return nil, err
}
uConn := utls.UClient(rawConn, &utls.Config{
ServerName: host,
InsecureSkipVerify: false,
}, utls.HelloChrome_Auto)
if err := uConn.HandshakeContext(ctx); err != nil {
rawConn.Close()
return nil, err
}
return uConn, nil
},
ForceAttemptHTTP2: true,
}
}
func main() {
client := &http.Client{
Transport: newUTLSTransport(),
Timeout: 30 * time.Second,
}
resp, err := client.Get("https://tls.peet.ws/api/all")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}utls.HelloChrome_Autoは最新のChromeプロファイルを自動追跡しますが、ライブラリのバージョンに依存するため、定期的にutlsをアップデートする必要があります。Safariのフィンガープリントが必要な場合はutls.HelloSafari_16_0を使用します:
uConn := utls.UClient(rawConn, &utls.Config{
ServerName: host,
}, utls.HelloSafari_16_0)プロファイル選択の目安は以下の通りです:
- HelloChrome_Auto:一般的なスクレイピング、SERP追跡、ECサイト価格監視
- HelloSafari_16_0:iOS向けターゲットやSafariユーザーが多いサイト
- HelloFirefox_120:Firefoxプロファイルが必要な場合
DialTLSContextの注意点
DialTLSContext内でHTTP/2ネゴシエーションを正しく行うには、ALPNがClientHelloプロファイルに含まれていることが重要です。uTLSのChromeプロファイルはh2,http/1.1をALPNに含むため、ForceAttemptHTTP2: trueを設定すればhttp.TransportがHTTP/2で通信します。ただし、HTTP/2のストリーム多重化やヘッダー圧縮の挙動もブラウザと一致させる必要がある場合は、後述するazuretls-clientの利用を検討してください。
上位レベルの代替:CycleTLSとazuretls-client
uTLSを直接http.Transportに接続するのは柔軟ですが、HTTP/2フレームの順序やヘッダー順序の偽装まで含めると実装が複雑になります。より高レベルなライブラリとして以下の2つがよく使われます:
CycleTLS
CycleTLSはuTLSをラップし、Ja3文字列とUser-Agentを指定するだけでリクエストを送れるGoライブラリです。HTTP/2に対応し、ヘッダー順序の制御も可能です:
import "github.com/Danny-Dasilva/CycleTLS/cycletls"
c := cycletls.Init()
resp, err := c.Do("https://tls.peet.ws/api/all", cycletls.Options{
Ja3: "771,4865-4866-...,-",
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...",
}, "GET")Ja3文字列を手動で指定できるため、特定のブラウザバージョンに固定したい場合に便利です。ただし、JA4が普及する中でJA3文字列のみの指定では将来的に不十分になる可能性があります。
azuretls-client
azuretls-clientは、uTLSベースでHTTP/2のストリーム順序、WINDOW_UPDATE、SETTINGSフレームの順序までChromeに合わせる高機能クライアントです。Goの標準net/httpインターフェースに近いAPIを持ち、プロキシ設定も組み込みでサポートしています:
import "github.com/Noooste/azuretls-client"
session := azuretls.NewSession()
session.SetOrderedHeaders(true)
session.Browser = azuretls.Chrome
resp, err := session.Get("https://tls.peet.ws/api/all")azuretls-clientはHTTP/2フレームレベルの偽装までカバーするため、Cloudflareの高度な検知を回避する際に有効です。ただし、ライブラリのメンテナンス頻度を確認し、Chromeプロファイルが最新かどうかを検証してください。
| ライブラリ | TLS偽装 | HTTP/2偽装 | プロキシ統合 | 適合ユースケース |
|---|---|---|---|---|
| utls (手動接続) | ClientHello完全制御 | 不可(別途必要) | 手動DialTLSContext | 最大限の制御が必要 |
| CycleTLS | JA3文字列指定 | 部分対応 | 組み込み | シンプルな偽装 |
| azuretls-client | ブラウザプロファイル | フレーム順序含む | 組み込み | 高度なWAF回避 |
フィンガープリント偽装だけでは不十分:IP評価の問題
uTLSでClientHelloをChromeと同一にしても、リクエスト元のIPがデータセンターブロックであればWAFは別のシグナルでブロックします。CloudflareはIP評価スコアをTLSフィンガープリントと組み合わせて判定するため、JIA3 + IP評価の両方が人間らしく見える必要があります。
ここでProxyHatの住宅プロキシが役立ちます。住宅プロキシは実際のISPに割り当てられたIPアドレスを使用するため、IP評価がデータセンタープロキシよりもはるかにクリーンです。ProxyHatのゲートウェイgate.proxyhat.com:8080をHTTPプロキシとしてuTLSクライアントに接続すれば、TLSフィンガープリントとIP評価を同時に人間らしくできます。
ProxyHat住宅プロキシ経由でuTLSクライアントを動かす実装例
以下は、uTLSのDialTLSContextでProxyHatのHTTPプロキシを経由し、geo-targetingでcountry-US、sticky sessionでsession-abc123を指定する例です:
package main
import (
"context"
"fmt"
"io"
"net"
"net/http"
"net/url"
"time"
utls "github.com/refraction-networking/utls"
)
const (
proxyHost = "gate.proxyhat.com"
proxyPort = "8080"
proxyUser = "user-country-US-session-abc123"
proxyPass = "pass"
)
func newProxiedUTLSTransport() *http.Transport {
proxyURL := &url.URL{
Scheme: "http",
Host: net.JoinHostPort(proxyHost, proxyPort),
User: url.UserPassword(proxyUser, proxyPass),
}
return &http.Transport{
Proxy: http.ProxyURL(proxyURL),
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
host, _, _ := net.SplitHostPort(addr)
// ProxyHatのHTTPプロキシに接続
proxyAddr := net.JoinHostPort(proxyHost, proxyPort)
rawConn, err := (&net.Dialer{Timeout: 15 * time.Second}).
DialContext(ctx, "tcp", proxyAddr)
if err != nil {
return nil, err
}
// CONNECTトンネルを確立
connectReq := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", addr, addr)
if _, err := rawConn.Write([]byte(connectReq)); err != nil {
rawConn.Close()
return nil, err
}
// プロキシの認証ヘッダーを送信(簡略化例)
// 実運用ではProxy-Authorization: Basic を含める
uConn := utls.UClient(rawConn, &utls.Config{
ServerName: host,
}, utls.HelloChrome_Auto)
if err := uConn.HandshakeContext(ctx); err != nil {
rawConn.Close()
return nil, err
}
return uConn, nil
},
ForceAttemptHTTP2: true,
}
}
func main() {
client := &http.Client{
Transport: newProxiedUTLSTransport(),
Timeout: 30 * time.Second,
}
resp, err := client.Get("https://tls.peet.ws/api/all")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}ユーザー名にcountry-US-session-abc123を指定することで、米国の住宅IPをsticky sessionで保持できます。同じセッションIDを使い続ける限り同じ出口IPが割り当てられるため、ログイン状態を維持するスクレイピングや、連続リクエストでIPが変わると問題になるサイトに適しています。詳細な設定はProxyHatドキュメントを参照してください。
SOCKS5プロキシが必要な場合はgate.proxyhat.com:1080を使用します:
socks5://user-country-DE-city-berlin:pass@gate.proxyhat.com:1080都市レベルのgeo-targeting(例:city-berlin)を使えば、より精度の高いローカライズが可能です。ProxyHatの対応ロケーションはロケーション一覧で確認できます。
JA4がJA3を置き換える中でParrotを最新に保つ
JA3は長年TLSフィンガープリントの標準でしたが、2023年末にFoxIOがJA4を公開し、徐々に移行が進んでいます。JA4はJA3よりも構造化されており、以下の点で異なります:
- ハッシュ化前の文字列を保持:JA3はハッシュのみを記録するが、JA4は構造化文字列(例:
t13d1516h2_8daaf6152771_b0da82dd1658)を保持するため、部分的な変更が追跡しやすい。 - SNIを含まない:JA3はSNIをハッシュに含めるため、ドメインごとにハッシュが変化する問題があった。JA4はSNIを除外し、クライアントのTLS実装のみを指紋化する。
- 拡張機能の順序を考慮:JA4は拡張機能をソートせずにそのままの順序で記録するため、拡張の並び順の偽装まで検証できる。
これは、uTLSのParrotプロファイルがChromeの最新ClientHelloに追従しているかどうかがより厳密に判定されることを意味します。実務上の推奨事項は以下の通りです:
- utlsを定期的にアップデート:
go get -u github.com/refraction-networking/utls@latestで最新のChromeプロファイルを取り込む。リリースノートでHelloChrome_Autoの対象バージョンを確認する。 - tls.peet.wsでJA4を定期検証:デプロイ後にJA3だけでなくJA4文字列がChromeの既知の値と一致することを確認する。
- azuretls-clientのブラウザプロファイル更新を追跡:azuretls-clientはHTTP/2フレームレベルの偽装も行うため、プロファイルの更新頻度が重要。
- セッションごとにJA3/JA4をランダム化しない:本物のブラウザはセッションごとにTLSフィンガープリントを変えないため、ランダム化は逆に不自然になる。
よくある間違いとエッジケース
1. uTLSだけ使ってプロキシを経由しない
データセンタープロキシやプロキシなしでuTLSを使うと、TLSフィンガープリントはChromeと一致してもIP評価で弾かれます。特にCloudflareはIP ASNの種類(ISP vs hosting)を判定材料に使うため、住宅プロキシの使用が実質的に必須です。
2. HTTP/2のヘッダー順序を無視
uTLSでClientHelloを偽装しても、net/httpのHTTP/2実装はヘッダーをアルファベット順で送信します。Chromeは特定の順序でヘッダーを送信するため、この差が追加のシグナルになります。azuretls-clientのSetOrderedHeaders(true)やCycleTLSのヘッダー順序制御を使って対処してください。
3. HelloChrome_Autoの更新タイミングを確認しない
HelloChrome_Autoはutlsのリリース時点での最新Chromeプロファイルに固定されます。utlsを半年以上更新せずに放置すると、ChromeのClientHelloが変化した後にJA4が古い値のままになり、検知される可能性が高まります。
4. sticky sessionとIPローテーションの混同
ProxyHatでsession-abc123を指定すると同じIPが保持されますが、セッションIDを変更しない限りIPがローテーションしません。リクエストごとにIPを変えたい場合はセッションIDを毎回変更し、ログイン状態を維持したい場合は固定してください。
ProxyHatのセットアップと関連リンク
ProxyHatの住宅プロキシを利用するには、プラン一覧からデータプランを選択し、ダッシュボードで認証情報を取得します。ゲートウェイgate.proxyhat.comのHTTPポート8080またはSOCKS5ポート1080を使用し、ユーザー名にgeo-targetingとセッションフラグを指定します。
スクレイピング用途の概要はWebスクレイピングのユースケースを、SERP追跡の具体的なアプローチはSERP追跡のユースケースを参照してください。プロキシの全設定パラメータは公式ドキュメントに記載されています。
Key Takeaways
- Goの
crypto/tlsはGREASEなし、固定暗号スイート順序、Chromeと異なる拡張構成のため、JA3/JA4で即座に識別される。refraction-networking/utlsのHelloChrome_AutoでClientHelloをChromeに偽装し、DialTLSContextでhttp.Transportに接続する。- TLS偽装だけでは不十分。ProxyHat住宅プロキシ(
gate.proxyhat.com:8080)でIP評価もクリーンに保つ必要がある。- HTTP/2ヘッダー順序やフレーム順序まで偽装する場合はazuretls-clientを検討する。
- JA4がJA3を置き換えつつあるため、utlsを定期的に更新し、tls.peet.wsでJA4文字列を検証する。
FAQ
Goのnet/http TLSフィンガープリントの修正とは何ですか?
Goの標準net/httpが使用するcrypto/tlsのClientHelloがChrome等のブラウザと構造的に異なり、WAFにJA3/JA4ハッシュで検知される問題を、uTLS等でブラウザのTLSフィンガープリントに偽装して修正することを指します。暗号スイート順序、GREASE値、拡張機能、key_shareの曲線セットを実際のブラウザに一致させる作業が中心です。
なぜこの問題がプロキシユーザーにとって重要なのですか?
WAFはTLSフィンガープリントとIP評価を組み合わせて判定します。住宅プロキシでクリーンなIPを使っていても、GoのデフォルトClientHelloがChromeと異なればブロックされます。逆にuTLSでフィンガープリントを偽装してもデータセンターIPではIP評価で弾かれます。両方を同時に解決する必要があるため、プロキシユーザーにとってTLSフィンガープリントの修正は必須の知識です。
この用途に最適なプロキシタイプはどれですか?
住宅プロキシが最適です。データセンタープロキシはIP ASNがhosting providerに分類されるため、WAFのIP評価スコアが低くなります。住宅プロキシは実際のISPに割り当てられたIPを使用するため、IP評価が高く、uTLSによるTLS偽装と組み合わせることで最も人間らしく見えます。ProxyHatの住宅プロキシはgate.proxyhat.com:8080でHTTPプロキシとして利用でき、国・都市レベルのgeo-targetingとsticky sessionに対応しています。
実装時にブロックを回避するにはどうすればよいですか?
第一に、uTLSのHelloChrome_AutoでClientHelloをChromeに偽装します。第二に、ProxyHat住宅プロキシを経由してIP評価をクリーンにします。第三に、HTTP/2のヘッダー順序をChromeに合わせるためazuretls-client等の使用を検討します。第四に、utlsを定期的に更新し、tls.peet.wsでJA3/JA4がChromeの値と一致することを検証します。最後に、対象サイトのrobots.txtと利用規約を遵守し、適切なレート制限を設けます。






