はじめに
IPv6を利用するネットワークでも、OCI Object Storageに接続できるかを確認したいと考えました。
Object Storageに追加されたDual-Stackエンドポイントを使い、Windows 11からIPv4とIPv6の両方で接続できることを検証します。
Object Storageは、ファイルなどのデータを保存するサービスです。
保存先の入れ物を「バケット」、保存するデータを「オブジェクト」と呼びます。
DNSによる接続先の確認から、HTTPS通信、非公開ファイルの取得までを順に試します。
DNSはホスト名からIPアドレスを調べる仕組み、HTTPSは暗号化されたHTTP通信です。
結果として、東京・大阪・Ashburnの3リージョンでIPv4とIPv6によるヘルスチェックが成功しました。
さらに東京リージョンでは、同じ非公開オブジェクトをIPv4とIPv6で取得し、SHA-256の一致を確認できました。
Dual-Stackエンドポイントとは
エンドポイントは、アプリケーションがサービスにアクセスするための接続先です。
この記事では、Object Storage APIのホスト名を指します。
APIは、プログラムからサービスを利用するための窓口です。
IPアドレスにはIPv4とIPv6の2種類があります。
Dual-Stackエンドポイントは、1つのホスト名でIPv4とIPv6の両方に対応する接続先です。
2つの方式で同時にファイルを送るという意味ではなく、クライアントが利用するIPバージョンで接続できます。
今までとの違い
2026年8月11日に、Object StorageのDual-Stackエンドポイント対応がリリースされました。
IPv4とIPv6で共通のホスト名を使用できます。
公式リリースノート
利用時のポイントは、従来のエンドポイントではなく、Dual-Stack用のエンドポイントを指定することです。
| 種類 | Object Storage Native APIのホスト名 |
|---|---|
| 従来のエンドポイント | objectstorage.<region>.oraclecloud.com |
| Dual-Stackエンドポイント | <namespace>.objectstorage.<region>.ds.oci.customer-oci.com |
| 比較項目 | 従来 | Dual-Stack |
|---|---|---|
| 利用できるIPバージョン | IPv4 | IPv4とIPv6 |
| DNSで取得するアドレス | Aレコード(IPv4) | AレコードとAAAAレコード(IPv6) |
| ホスト名へのネームスペース指定 | 上記の従来形式では不要 | 必要 |
ネームスペースは、Object Storageでテナントを区別するための名前です。
Dual-Stack用URLへ変更しても、バケットや保存済みファイルをIPv6専用に作り直すわけではありません。
同じデータへ、別の通信方式でもアクセスできるようになります。
<namespace>はObject Storageネームスペース、<region>はap-tokyo-1などのリージョン識別子です。
OC1商用レルムでは、従来のoraclecloud.comエンドポイントはIPv4のみです。
Dual-Stack対応には、ネームスペースをホスト名に含めたds.oci.customer-oci.com形式を使用します。
公式ドキュメント:エンドポイント形式
どのような使われ方が期待されるか
主な利用場面は、IPv6を導入した社内ネットワークやアプリケーションからのデータ取得です。
IPv4の利用者を残しながら、IPv6の利用者にも同じホスト名を案内できるため、段階的な移行にも使えます。
これは対応仕様から考えられる利用例です。
検証の目的と想定される結果
目的は、接続先がDNSに登録されているだけでなく、IPv6で実際のファイルまで取得できることを確認することです。
| 検証 | 想定される結果 | 判定方法 |
|---|---|---|
| DNS | Dual-StackでIPv4/IPv6のアドレスを取得可能 | A/AAAAレコードを確認 |
| HTTPS | 同じホスト名へIPv4/IPv6で接続可能 | curlの終了コード0、HTTP 200、接続先のIPバージョンを確認 |
| 非公開ファイルの取得 | 同じファイルをIPv4/IPv6で取得可能 | HTTP 200、サイズとSHA-256の一致を確認 |
SHA-256は、ファイルの内容から計算するハッシュ値です。
取得前後で値を比較し、同じ内容が得られたかを確認します。
検証構成
Windowsからインターネット経由でObject Storageへ接続する構成です。
ヘルスチェックは3リージョン、非公開ファイルの取得は東京リージョンで行います。
非公開ファイルの取得時だけ、読取り権限を持つユーザーの署名を付けます。
IPv4とIPv6で同じファイルを取得し、内容を比較します。
検証環境
| 項目 | 内容 |
|---|---|
| クライアント | Windows 11 Pro、OSバージョン10.0.26200
|
| シェル | PowerShell 7.6.5 |
| HTTPクライアント | curl 8.21.0、Schannel、IPv6対応 |
| 署名生成 | Python 3.12、OCI Python SDK 2.185.2 |
| 接続先 | OCI Object Storageのパブリック・エンドポイント |
| ヘルスチェック | 東京、大阪、Ashburn |
| オブジェクト取得 | 東京、非公開バケット、37バイトのテキスト |
今回使用したのはObject StorageのNative APIです。
Swift API、S3互換API、プライベート・エンドポイントは検証していません。
必要な前提は以下です。
- WindowsからインターネットへのIPv6通信が可能であること。
- 対象テナントのObject Storageネームスペースが分かること。
- オブジェクト取得まで行う場合は、取得対象と、そのオブジェクトを読み取れる認証情報があること。
今回、ヘルスチェックには認証ヘッダーを付けずにアクセスできました。
非公開オブジェクトの取得には、OCIのAPI署名を付けています。
検証方法と結果
1. Windows側のIPv6環境を確認する
まず、curlがIPv6に対応していることを確認します。
PowerShellのエイリアスとの混同を避けるため、コマンドはcurl.exeと明示します。
curl.exe --version
今回の出力では、FeaturesにIPv6が含まれていました。
curl 8.21.0 (Windows) libcurl/8.21.0 Schannel zlib/1.3.2 WinIDN WinLDAP
Features: alt-svc AsynchDNS HSTS HTTPS-proxy IDN IPv6 Kerberos Largefile libz SPNEGO SSL SSPI threadsafe Unicode UnixSockets
必要に応じて、IPv6アドレスと既定経路も確認します。
Get-NetIPAddress -AddressFamily IPv6
Get-NetRoute -AddressFamily IPv6 -DestinationPrefix '::/0'
今回の環境では、グローバルIPv6アドレスとIPv6の既定経路が存在していました。
ただし、アドレスや経路があることだけではHTTPS通信の成功は確認できないため、以降で実際に接続します。
2. A/AAAAレコードを確認する
東京リージョンを例に、接続先を組み立てます。
$Namespace = '<namespace>'
$Region = 'ap-tokyo-1'
$LegacyHost = "objectstorage.$Region.oraclecloud.com"
$DualHost = "$Namespace.objectstorage.$Region.ds.oci.customer-oci.com"
Resolve-DnsName -Name $LegacyHost -Type A -DnsOnly
# 従来形式でAAAAレコードがない場合は、該当レコードなしのエラーが返る
Resolve-DnsName -Name $LegacyHost -Type AAAA -DnsOnly
Resolve-DnsName -Name $DualHost -Type A -DnsOnly
Resolve-DnsName -Name $DualHost -Type AAAA -DnsOnly
今回のDNS確認結果は以下でした。
| リージョン | 従来:A | 従来:AAAA | Dual-Stack:A | Dual-Stack:AAAA |
|---|---|---|---|---|
東京 ap-tokyo-1
|
あり | なし | あり | あり |
大阪 ap-osaka-1
|
あり | なし | あり | あり |
Ashburn us-ashburn-1
|
あり | なし | あり | あり |
Dual-Stack用のホスト名では、IPv4のAレコードとIPv6のAAAAレコードの両方を取得できました。
DNSの応答件数やアドレスは変わり得るため、特定のIPアドレスを固定して利用するのではなく、ホスト名を指定します。
3. IPv4とIPv6を明示してHTTPS接続する
DNSの確認だけでは実際にどちらのIPアドレスで通信したか分からないため、curlの-4と-6を使い分けます。
アクセス先は、公式ドキュメントで接続確認用に示されている/v2:healthcheckです。
公式ドキュメント:IPアドレス互換性の確認
$HealthUrl = "https://$DualHost/v2:healthcheck"
# IPv4に限定
curl.exe -4 --silent --show-error --connect-timeout 8 --max-time 15 `
--output NUL --write-out 'HTTP=%{http_code} remote=%{remote_ip}\n' `
$HealthUrl
$LASTEXITCODE
# IPv6に限定
curl.exe -6 --silent --show-error --connect-timeout 8 --max-time 15 `
--output NUL --write-out 'HTTP=%{http_code} remote=%{remote_ip}\n' `
$HealthUrl
$LASTEXITCODE
東京リージョンでの結果です。
接続先IPアドレスは文書用アドレスへ置換しています。
# IPv4
HTTP=200 remote=192.0.2.10
0
# IPv6
HTTP=200 remote=2001:db8:10::10
0
HTTP=200に加え、IPv6の接続先アドレスが表示され、curlの終了コードが0になりました。
3リージョンの結果をまとめます。
| リージョン | 従来エンドポイント:IPv4 | Dual-Stack:IPv4 | Dual-Stack:IPv6 | IPv6の接続先(置換済み) |
|---|---|---|---|---|
| 東京 | HTTP 200 | HTTP 200 | HTTP 200 | 2001:db8:10::10 |
| 大阪 | HTTP 200 | HTTP 200 | HTTP 200 | 2001:db8:20::10 |
| Ashburn | HTTP 200 | HTTP 200 | HTTP 200 | 2001:db8:30::10 |
同じDual-Stack用ホスト名で、IPv4とIPv6の両方からHTTPS通信できることが分かりました。
なお、ヘルスチェックの成功は、バケット内のデータを読み取れることまで保証するものではありません。
次に、認証が必要なオブジェクト取得を確認します。
4. 非公開オブジェクトをIPv4とIPv6で取得する
検証データ
東京リージョンの非公開バケットに、以下の内容を持つipv6-check.txtを用意しました。
OCI Object Storage IPv6 verification
文字コードはUTF-8、BOMなし、末尾はLFで、サイズは37バイトです。
PowerShellで同じファイルを作る場合は、次のように改行と文字コードを明示できます。
[IO.File]::WriteAllText(
(Join-Path $PWD 'ipv6-check.txt'),
"OCI Object Storage IPv6 verification`n",
[Text.UTF8Encoding]::new($false)
)
Get-FileHash -Algorithm SHA256 .\ipv6-check.txt
既存の検証用バケットなどへアップロードし、そのオブジェクトを読み取れるユーザーのAPIキー認証を使います。
バケットの公開設定や事前認証済リクエスト(PAR)は使用していません。
署名付きリクエストを作る理由
今回の取得先URLは次の形式です。
https://<namespace>.objectstorage.ap-tokyo-1.ds.oci.customer-oci.com/n/<namespace>/b/<bucket>/o/ipv6-check.txt
非公開オブジェクトの取得には、ユーザー認証と読取り権限が必要です。
今回はOCI Python SDKでリクエストに署名し、通信そのものはcurl.exe -4とcurl.exe -6で実行しました。
これにより、認証したうえで通信のIPバージョンも明示できます。
API署名は、リクエストが登録済みのユーザーから送られたことを確認するための仕組みです。
SDKは署名を作るためのライブラリで、この記事の手順はChatGPTやCodexを使わずに実行できます。
署名は実際にアクセスするDual-StackのURLに対して生成します。
署名後にホスト名を書き換えないようにします。
使用した再現用スクリプトを末尾に掲載しています。
通常のOCI設定ファイルとAPI署名キーを用意したうえで、以下のように実行します。
python -m pip install 'oci==2.185.2'
python .\check-object.py `
--region ap-tokyo-1 `
--namespace '<namespace>' `
--bucket '<bucket>' `
--object ipv6-check.txt `
--output-dir .\object-downloads
既定では~/.oci/configのDEFAULTプロファイルを読み取ります。
別の設定を使う場合は、--config-fileと--profileを指定します。
APIキー認証用の設定例です。
[DEFAULT]
user=<user_ocid>
fingerprint=<api_key_fingerprint>
tenancy=<tenancy_ocid>
region=ap-tokyo-1
key_file=<private_key_file_path>
取得結果
| 方式 | HTTP | 接続先(置換済み) | サイズ | SHA-256 |
|---|---|---|---|---|
| 従来エンドポイント・IPv4 | 200 | 192.0.2.10 |
37バイト | 下記と一致 |
| Dual-Stack・IPv4 | 200 | 192.0.2.10 |
37バイト | 下記と一致 |
| Dual-Stack・IPv6 | 200 | 2001:db8:10::10 |
37バイト | 下記と一致 |
SHA-256は、元ファイルと各方式で取得したファイルのすべてで一致しました。
d11d4551eab1fe7820f73827ba6c89bc649a0cd31c2616923f52b0a7f00b09d1
再現用スクリプトの出力例です。
接続先IPアドレスのみ置換しています。
{"family": "IPv4", "curl_exit": 0, "http_code": 200, "remote_ip": "192.0.2.10", "address_family_verified": true, "bytes": 37, "sha256": "d11d4551eab1fe7820f73827ba6c89bc649a0cd31c2616923f52b0a7f00b09d1"}
{"family": "IPv6", "curl_exit": 0, "http_code": 200, "remote_ip": "2001:db8:10::10", "address_family_verified": true, "bytes": 37, "sha256": "d11d4551eab1fe7820f73827ba6c89bc649a0cd31c2616923f52b0a7f00b09d1"}
{"both_transports_verified_and_hashes_match": true}
IPv6アドレスでHTTP 200が返ることに加えて、取得した内容もIPv4と同一であることを確認できました。
想定との比較
| 検証 | 想定 | 実測結果 | 比較 |
|---|---|---|---|
| DNS | Dual-StackでA/AAAAを取得 | 3リージョンとも取得可能 | 一致 |
| HTTPS | IPv4/IPv6ともHTTP 200 | 3リージョンとも成功、接続先のIPバージョンも確認 | 一致 |
| 非公開ファイル | 両方式で同じ内容を取得 | 東京で37バイト、SHA-256一致 | 一致 |
考察と検証範囲
今回の検証では、Windows 11からObject StorageのDual-StackエンドポイントへIPv4・IPv6の両方でHTTPS接続できました。
東京では、非公開オブジェクトの署名付きGETも両方で成功しています。
この結果から、今回の環境では、通信方式をIPv6にしても同じ非公開ファイルへアクセスできることが分かりました。
IPv6対応と読取り権限は別の条件なので、DNS、HTTPS、認証付き取得を分けて確認すると、どこまで動作したか判断しやすくなります。
一方、次の点は区別する必要があります。
- DNSのAAAAレコード取得だけでは、IPv6のHTTPS通信成功を確認したことにはならない。
- IPバージョンを指定しない通信の成功だけでは、IPv6を使用したか判断できない。
- ヘルスチェックの成功だけでは、非公開オブジェクトの取得権限は確認できない。
- 今回は小さなファイルの取得確認であり、性能・大容量転送・IPv6でのPUT/DELETEは検証していない。
また、公式資料では、Virtual-hosted形式のURLおよびプライベート・エンドポイントはDual-Stack/IPv6の対象外とされています。
別の接続方式へ展開する場合は、その方式の対応状況を確認してください。
公式資料の制約事項
再現用スクリプト
以下をcheck-object.pyとして保存します。
取得対象は検証用オブジェクトにし、出力先には未使用のディレクトリを指定してください。
署名済みヘッダーはcurlの標準入力へ渡し、画面には出力しません。
この例はAPIキー認証向けです。
check-object.py
"""Download a private OCI object with curl, forcing IPv4 and IPv6 separately.
Requires: pip install oci
Authentication headers go to curl through stdin and are never displayed.
"""
import argparse
import hashlib
import ipaddress
import json
import subprocess
from pathlib import Path
from urllib.parse import quote
import oci
from oci._vendor import requests
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--config-file', default=oci.config.DEFAULT_LOCATION)
parser.add_argument('--profile', default='DEFAULT')
parser.add_argument('--region', required=True)
parser.add_argument('--namespace', required=True)
parser.add_argument('--bucket', required=True)
parser.add_argument('--object', required=True)
parser.add_argument('--output-dir', default='object-downloads')
args = parser.parse_args()
config = oci.config.from_file(args.config_file, args.profile)
signer = oci.signer.Signer(config['tenancy'], config['user'], config['fingerprint'], config['key_file'], pass_phrase=config.get('pass_phrase'))
endpoint = f'https://{args.namespace}.objectstorage.{args.region}.ds.oci.customer-oci.com'
url = endpoint + '/n/' + quote(args.namespace, safe='') + '/b/' + quote(args.bucket, safe='') + '/o/' + quote(args.object, safe='')
out = Path(args.output_dir)
out.mkdir(parents=True, exist_ok=True)
results = []
for family in (4, 6):
req = requests.Request('GET', url).prepare()
signer(req)
curl_config = '\n'.join(['url = ' + json.dumps(url)] + ['header = ' + json.dumps(f'{k}: {v}') for k, v in req.headers.items()]) + '\n'
body = out / f'object-ipv{family}.bin'
if body.exists():
raise SystemExit(f'Use a new output directory: {body} already exists')
p = subprocess.run(['curl.exe', '-q', '--config', '-', f'-{family}', '--silent', '--show-error', '--connect-timeout', '8', '--max-time', '20', '--output', str(body), '--write-out', '%{json}'], input=curl_config, capture_output=True, text=True)
stats = json.loads(p.stdout) if p.stdout else {}
success = p.returncode == 0 and stats.get('http_code') == 200
remote = stats.get('remote_ip', '')
family_verified = bool(remote) and ipaddress.ip_address(remote).version == family
result = {'family': f'IPv{family}', 'curl_exit': p.returncode, 'http_code': stats.get('http_code'), 'remote_ip': remote, 'address_family_verified': family_verified, 'bytes': body.stat().st_size if success else None, 'sha256': hashlib.sha256(body.read_bytes()).hexdigest() if success else None}
results.append(result)
print(json.dumps(result, ensure_ascii=False))
matched = all(r['http_code'] == 200 and r['curl_exit'] == 0 and r['address_family_verified'] for r in results) and results[0]['sha256'] == results[1]['sha256']
print(json.dumps({'both_transports_verified_and_hashes_match': matched}))
raise SystemExit(0 if matched else 1)
if __name__ == '__main__':
main()