技術 約9分で読めます

VLESS + REALITY を Xray-core だけで立てて偽装まで確認した(パネルなし・root不要の最小構成)

いけさん目次

前にVLESS + REALITY の概要記事を書いたが、あれはリソース紹介がメインで自分の手は動かしていなかった。 今回は手元のLinuxマシンに実際に立ててみた。パネル(3X-UI)は使わず、Xray-core のバイナリと設定ファイルだけでどこまで簡単に立つかを確かめる。

root もドメインも要らず、バイナリを1つ落として設定ファイルを2つ書くだけで VLESS + REALITY は立つ。 鍵生成から SOCKS 経由の疎通確認まで10分かからなかった。 ただし新しめのバージョンだと出力フォーマットが変わっていたり、ポート選びに警告が出たりと、古い手順のコピペでは引っかかる箇所がいくつかあった。

そして疎通が通っただけでは「立った」としか言えない。REALITY の肝は、外から覗いたときに本物のサイトにしか見えないことにある。openssl s_client で外部プローブの視点を再現して、偽装が効いているところまで確認した。

今回はあくまで「立てやすさ」を見るためのローカル検証で、サーバーもクライアントも同じマシン上に置いている。地理的に出口を変える=中国の外に出られるかという本番検証は、次にAlibaba Cloudで試す。

検証環境

サーバーもクライアントも、ジャンクのDell Latitude 5310に Mageia Linux を入れた検証機の上で動かしている。

項目内容
モデルDell Latitude 5310
CPUIntel Core i5-10310U (Comet Lake, 4C/8T, 1.7-4.4GHz)
メモリ約16GB
ストレージNVMe SSD 238GB
OSMageia 9 (Xfce) / x86_64
権限一般ユーザー(root なし)
Xray-corev26.3.27(執筆時点の最新)
構成パネルなし・systemd登録なし(フォアグラウンドで起動して確認)

REALITY は実在サイトの TLS 証明書を借りて偽装するプロトコルで、自分でドメインも証明書も用意しなくていい。仕組みは概要記事に書いたのでここでは省く。

全体の流れ

立てるのに必要なステップはこれだけ。

flowchart TD
  A[Xray-core バイナリを取得] --> B[鍵ペア / UUID / shortId を生成]
  B --> C[サーバー設定を書く<br/>VLESS + REALITY の受信側]
  C --> D[設定をテスト<br/>xray run -test]
  D --> E[クライアント設定を書く<br/>SOCKS 入口 + REALITY 出口]
  E --> F[両方を起動して疎通確認]
  F --> G[openssl で偽装を確認<br/>本物の証明書が返るか]

バイナリの取得

GitHub のリリースから Linux 64bit 版を落として展開するだけ。インストーラもパッケージマネージャも使わない。

mkdir -p ~/vless-test && cd ~/vless-test
curl -sL -o xray.zip https://github.com/XTLS/Xray-core/releases/download/v26.3.27/Xray-linux-64.zip
unzip -o xray.zip
chmod +x xray
./xray version
Xray 26.3.27 (Xray, Penetrates Everything.) d2758a0 (go1.26.1 linux/amd64)

zip の中に xray 本体と geoip.dat / geosite.dat(ルーティング用の地域データ)が入っている。今回の最小構成では geo データは使わないが、同梱されているので消さずに置いておく。

鍵・UUID・shortId の生成

REALITY には x25519 の鍵ペア、クライアント識別用の UUID、認証用の shortId が要る。すべて xray コマンドで出せる。

./xray x25519        # REALITY 用の鍵ペア
./xray uuid          # クライアント UUID
od -An -tx1 -N8 /dev/urandom | tr -d ' \n'   # shortId(8バイトの hex)

xray x25519 の出力がこれ。

PrivateKey: IILkxOpNRR72F9h9k6yVWB537l6Df4fYxcunbnzLNUE
Password (PublicKey): p8MIhWYnijydf3ofqPlnf3p7OquIyXn99yU5WB5y0zo
Hash32: Kuy1X9sdUGaHU225_JLxNgc_OmKcOzAyYi9rIRYFfsM

ここが最初の詰まりどころ。 古い記事やパネルのスクショだと2行目は Public key: という表記なのに、v26 系では Password (PublicKey): に変わっている。 クライアント側に書くのはこの Password (PublicKey) の値(公開鍵)で、Hash32 ではない。Hash32 を貼って繋がらず悩む、というのが起こりやすい。サーバーに書くのが PrivateKey、クライアントに書くのが Password (PublicKey) と覚えておく。

PrivateKey は名前の通り秘密鍵なので、本番では絶対に公開しないこと。この記事の値は使い捨てのローカル検証で生成して既に破棄したもの。

サーバー設定

VLESS + REALITY の受信設定(inbounds)を1つ書く。今回は root を使わないので 127.0.0.1:8443 で待ち受ける。偽装先(dest / serverNames)は TLS 1.3 と HTTP/2 が確実な www.cloudflare.com にした。

{
  "log": { "loglevel": "warning" },
  "inbounds": [
    {
      "listen": "127.0.0.1",
      "port": 8443,
      "protocol": "vless",
      "settings": {
        "clients": [
          { "id": "97e1a708-...", "flow": "xtls-rprx-vision" }
        ],
        "decryption": "none"
      },
      "streamSettings": {
        "network": "tcp",
        "security": "reality",
        "realitySettings": {
          "show": false,
          "dest": "www.cloudflare.com:443",
          "xver": 0,
          "serverNames": ["www.cloudflare.com"],
          "privateKey": "IILkxOpNRR72F9h9k6yVWB537l6Df4fYxcunbnzLNUE",
          "shortIds": ["2c26a4c08d1cce62"]
        }
      }
    }
  ],
  "outbounds": [{ "protocol": "freedom" }]
}

押さえるポイントはこのあたり。

項目設定
privateKeyサーバー側に置く(公開鍵ではない)
dest / serverNamesクライアントの serverName と一致させる
flowxtls-rprx-vision(サーバーとクライアントで揃える)
outboundsfreedom(受け取った通信をそのまま外へ流す)

書いたら起動前に設定ファイルを検証する。

./xray run -test -c server.json
[Warning] infra/conf: REALITY: Listening on non-443 ports may get your IP blocked by the GFW
Configuration OK.

Configuration OK が出れば設定の構文は通っている。 ここで2つ目の詰まりどころ。非443ポートで待ち受けると GFW にIPをブロックされうる、という警告が出る。 REALITY は「443で普通の HTTPS サイトに見せる」のが本質なので、本番は443で待ち受けることになる。この設計上の前提が起動前の警告で出てくる。今回は検証なので8443のまま進めた。

クライアント設定

クライアント側は SOCKS の入口を作り、出口を VLESS + REALITY にする。curl などをこの SOCKS プロキシに向ければ、通信が REALITY 経由でサーバーに流れる。

{
  "log": { "loglevel": "warning" },
  "inbounds": [
    { "listen": "127.0.0.1", "port": 10808, "protocol": "socks", "settings": { "udp": true } }
  ],
  "outbounds": [
    {
      "protocol": "vless",
      "settings": {
        "vnext": [
          {
            "address": "127.0.0.1",
            "port": 8443,
            "users": [
              { "id": "97e1a708-...", "encryption": "none", "flow": "xtls-rprx-vision" }
            ]
          }
        ]
      },
      "streamSettings": {
        "network": "tcp",
        "security": "reality",
        "realitySettings": {
          "serverName": "www.cloudflare.com",
          "fingerprint": "chrome",
          "publicKey": "p8MIhWYnijydf3ofqPlnf3p7OquIyXn99yU5WB5y0zo",
          "shortId": "2c26a4c08d1cce62"
        }
      }
    }
  ]
}

サーバーとの対応で間違えやすいのはここ。

クライアント側設定する値
publicKeyサーバーの Password (PublicKey) の値
serverNameサーバーの serverNames と一致
shortIdサーバーの shortIds のどれか
fingerprintchrome(TLS の ClientHello を Chrome に偽装)

公開鍵・shortId・偽装先のどれか1つでもサーバーとズレると、エラーログを出さずに無言で繋がらないことがある。繋がらないときはまずこの3つの一致を疑う。

起動と疎通確認

サーバーとクライアントを両方バックグラウンドで起動する。

./xray run -c server.json > server.log 2>&1 &
./xray run -c client.json > client.log 2>&1 &

ss で両方のポートが LISTEN しているか確認。

LISTEN 127.0.0.1:8443    users:(("xray",...))
LISTEN 127.0.0.1:10808   users:(("xray",...))

SOCKS プロキシ(10808)経由で外に出られるか curl で確認する。

curl -s -x socks5h://127.0.0.1:10808 https://api.ipify.org?format=json
{"ip":"180.4.30.248"}

応答が返ってきた。サーバーとクライアントのログにも中継の記録が残る。

# client.log
from tcp:127.0.0.1:52466 accepted tcp:api.ipify.org:443
# server.log
from 127.0.0.1:38450 accepted tcp:api.ipify.org:443

クライアントが受けた接続が、REALITY を抜けてサーバー側でも受理されている。 出口IPが直結時と同じなのは、今回サーバーを同じマシンに置いているから当然で、ここでは「経路が成立したか」だけを見ている。

偽装が効いているかを確認する

ここまでで「通信は通る」ことは分かった。だが REALITY を使う理由はそこではない。肝心なのは、外から覗かれたときに、ただの HTTPS サイトにしか見えないこと。疎通だけ見て満足すると、その偽装が効いているかを確かめないまま終わってしまう。

確認には、認証鍵を持たない第三者の視点が要る。中国の GFW はあやしいサーバーに対して、自分でも接続して正体を探る「アクティブプローブ」をかけてくる。これと同じことを openssl s_client でやる。openssl は REALITY の鍵を知らないので、ちょうど鍵を持たないプローブと同じ扱いになる。

REALITY サーバーは認証に失敗した接続を、そのまま偽装先(dest)へ素通しする。つまり鍵を持たない相手には、偽装先の本物のサーバーがそのまま応答する。狙い通りなら、自前サーバーに繋いだのに本物の www.cloudflare.com の証明書が返ってくるはずだ。

まず比較用に、本物の www.cloudflare.com:443 の証明書を見ておく。

echo "Q" | openssl s_client -connect www.cloudflare.com:443 -servername www.cloudflare.com 2>/dev/null \
  | grep -E "^(subject=|issuer=)"
subject=CN = www.cloudflare.com
issuer=C = US, O = Google Trust Services, CN = WE1

次に、立てたばかりの自前サーバー(127.0.0.1:8443)に、同じ SNI を指定して接続する。

echo "Q" | openssl s_client -connect 127.0.0.1:8443 -servername www.cloudflare.com 2>/dev/null \
  | grep -E "^(subject=|issuer=|Verify return code)"
subject=CN = www.cloudflare.com
issuer=C = US, O = Google Trust Services, CN = WE1
Verify return code: 0 (ok)

subjectissuer も本物と完全に一致した。発行者は本物の認証局(Google Trust Services)で、Verify return code: 0 (ok) なのでチェーン検証も通っている。証明書の有効期限や SAN まで見ても本物そのものだった。

echo "Q" | openssl s_client -connect 127.0.0.1:8443 -servername www.cloudflare.com 2>/dev/null \
  | openssl x509 -noout -dates -ext subjectAltName
notBefore=May  7 16:54:23 2026 GMT
notAfter=Aug  5 17:54:15 2026 GMT
X509v3 Subject Alternative Name:
    DNS:www.cloudflare.com

自分のマシンの 127.0.0.1:8443 に繋いだだけなのに、本物の Cloudflare のエンドポイントと見分けがつかない。プローブをかけても「普通に Cloudflare を使っているサーバー」にしか見えない、というのが REALITY の偽装が効いている状態だ。

サーバー側のログで挙動を裏取りする

サーバーの logleveldebug に上げて同じプローブを投げると、REALITY が内部で何をしているかが見える。

REALITY remoteAddr: 127.0.0.1:35068	forwarded SNI: www.cloudflare.com
[Info] transport/internet/tcp: REALITY: processed invalid connection from 127.0.0.1:35068: authentication failed or validation criteria not met

authentication failed(鍵を持たないので認証失敗)として扱い、SNI をそのまま偽装先へ転送(forwarded SNI)している。鍵なしのプローブには本物を見せる、という設計どおりの動きだ。

ためしに偽装先と違う SNI(example.com)を投げると、別の理由で弾かれる。

REALITY remoteAddr: 127.0.0.1:35076	forwarded SNI: example.com
[Info] transport/internet/tcp: REALITY: processed invalid connection from 127.0.0.1:35076: server name mismatch: example.com

server name mismatch と記録される。どちらの場合も偽装先へ素通しになるので、外から見える応答は常に本物の TLS エンドポイントのまま。正規のクライアント(鍵を持つ側)だけが、認証を通った先で実際のプロキシ通信に入れる。

確認が終わったらプロセスを止める。

pkill -f 'xray run'

やってみて

立てやすさという点では拍子抜けするほど簡単だった。 root もドメインも証明書も要らず、バイナリ1つと設定ファイル2つで VLESS + REALITY の経路が成立する。3X-UI のようなパネルは複数ユーザー管理やトラフィック監視には便利だが、1人で1経路を立てて確かめるだけなら、コアだけのほうがむしろ構造が見えていい。

引っかかったのは手順そのものより、バージョン差分だった。x25519 の出力名が変わっていたり、非443の警告が出たりと、古いコピペが通用しない箇所が地味にある。鍵の対応(秘密鍵=サーバー、公開鍵=クライアント)と偽装先の一致さえ押さえれば、あとは詰まらない。

偽装の確認まで含めても手間は小さい。openssl s_client を1回叩いて、本物のサイトと同じ証明書が返るかを見るだけで、外から覗いたときの見え方がはっきりする。ここを飛ばすと、トンネルは通るのに偽装が崩れている、という状態に気づけない。


次は本番。同じ構成を Alibaba Cloud のインスタンスに立てて、中国の内側から外に出られるかを試す。今回は「立つか」だけだったが、次は「中華圏から脱出できるか」を見る。

関連記事