1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

VPC Lambdaの送信元IPはソケットから取れない ― Hyperplane ENIとフローログで確かめた

1
Last updated at Posted at 2026-09-15

はじめに

Transit Gateway(TGW)経由の通信がどう処理されるかを検証する機会がありました。
検証の中で「VPCにアタッチしたLambdaから送ったパケットが、TGWフローログ上でどう記録されるか」を追いたかったのですが、そのためにはフローログの中からLambdaの通信を特定する必要があります。

特定の手がかりとして一番わかりやすいのは Lambdaの送信元IP です。そこで、Lambdaのコード内で自分の送信元IPを取得してログに出しておき、その値でフローログを検索しようとしました。

……が、ソケットから取得したIPはフローログのどこにも出てきませんでした。

本記事では、この現象の原因(Hyperplane ENI)と、Lambdaの実際の送信元IPを特定する方法を、個人環境で再現した検証結果とあわせてまとめます。

検証を簡易に実施するため、本記事の数値はすべて TGWを使わずVPC単体で再現した環境 のものです。TGWフローログの代わりにVPCフローログを使っていますが、原因となる仕組みは同じです。

やろうとしたこと

やりたかったことは次の流れです。

  1. Lambda(VPCアタッチ)から、TGWの先にある対向リソースへパケットを送信する
  2. 送信後、TGWフローログを見てその通信を追跡する
  3. 追跡の手がかりとして、Pythonの socketgetsockname() など)でLambda自身の送信元IPを事前に取得しておく

3.は、よくある「自分のIPを取得する」書き方です。

import socket

# TCPで接続したソケットのローカルアドレス
with socket.create_connection((dest_host, dest_port)) as s:
    print(s.getsockname())  # (送信元IP, 送信元ポート)

# UDPソケットをconnectしてローカルアドレスを見る(パケットは送信されない)
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
    s.connect((dest_host, dest_port))
    print(s.getsockname())

起きた問題

ソケットから取得したIPと、フローログに記録されていた送信元IPが一致しませんでした。

再現環境での値を並べると次のとおりです。

取得方法 送信元IP 送信元ポート
Lambda内:TCPソケットの getsockname() 169.254.100.6 52910
Lambda内:UDP connect して getsockname() 169.254.100.6 -
Lambda内:gethostbyname(gethostname()) 169.254.8.241 -
VPCフローログの srcaddr / srcport 10.0.1.106 53526

ソケットから取れたのは 169.254.0.0/16(リンクローカルアドレス)で、Lambdaをアタッチしたサブネット(10.0.1.0/24, 10.0.2.0/24)のアドレスですらありません。
しかも 送信元ポートまで違います

「あれ、ソケットのIPは何を指しているんだ?」というのが調査のきっかけです。

原因調査:Hyperplane ENIの仕組み

Lambdaの実行環境はユーザーのVPCの中にはいない

AWSのドキュメントには次のように書かれています。

Every Lambda function runs inside a VPC that is owned and managed by the Lambda service. These VPCs are maintained automatically by Lambda and are not visible to customers.
Giving Lambda functions access to resources in an Amazon VPC

つまり、Lambdaを「VPCにアタッチ」しても、関数のコードが動いている実行環境そのものは AWSが管理するLambda用のVPC の中にあります。

ユーザーのVPCに見えているのはHyperplane ENI

では、ユーザーのVPCには何があるのかというと、Hyperplane ENI です。

  • Lambdaサービスが作成・管理するENIで、ユーザーのVPCのサブネット内に作られる
  • サブネットとセキュリティグループの組み合わせ ごとに作られ、同じ組み合わせを使う 他の関数とも共有される
  • 1つのENIで最大65,000接続/ポートを扱い、足りなければLambdaが自動でENIを増やす

The first time you attach a function to a VPC using a particular subnet and security group combination, Lambda creates a Hyperplane ENI. Other functions in your account that use the same subnet and security group combination can also use this ENI.
(同上)

実行環境 → Hyperplane ENI の間でNATされている

2019年のLambda VPCネットワーク改善のアナウンスでは、次のように説明されています。

we are now leveraging Hyperplane to provide NAT capabilities from the Lambda VPC to customer VPCs.
Announcing improved VPC networking for AWS Lambda functions

Lambda用VPCからユーザーのVPCへ抜けるところでNAT(いわゆる V2N: VPC-to-VPC NAT)が行われ、送信元が Hyperplane ENIのプライベートIP に変換されてから対向に届きます。

[Lambda実行環境]              [ユーザーのVPC]                         [対向リソース]
 169.254.100.6:52910  ──NAT──▶  Hyperplane ENI 10.0.1.106:53526  ──▶  10.0.10.4:8080
 (Lambda管理VPC側)             ↑ フローログ・対向から見えるのはこちら

つまり、

  • ソケットが返すのは、Lambda実行環境側のローカルなアドレス
  • フローログや対向リソースから見える送信元は、NAT後のHyperplane ENIのアドレス

であり、両者は別物です。今回の再現ではIPだけでなくポートも変換されていました。

検証環境

TGWの構築はハードルが高いので、VPC単体で次の構成を作りました(Terraform)。

VPC 10.0.0.0/16(IGW/NATなし)
├─ Lambda用サブネット 10.0.1.0/24 (AZ-a), 10.0.2.0/24 (AZ-c)
│    Lambda: lambda-srcip-a / lambda-srcip-b(同じサブネット+SG)
│
└─ 対向サブネット 10.0.10.0/24
     └─ EC2 (t4g.nano): 受け付けた接続の送信元IP:ポートをJSONで返すHTTPサーバー :8080

VPCフローログ(ALL, 最大集約間隔60秒)→ CloudWatch Logs

ポイントは次の3つです。

  • 同じサブネット+SGで 関数を2つ 作り、Hyperplane ENIの共有を確認できるようにした
  • 対向EC2を「見えた送信元をそのまま返すサーバー」にして、受信側から見た送信元 をLambdaのレスポンスで回収できるようにした
  • フローログはカスタムフォーマットで pkt-srcaddr / flow-direction / instance-id も出力した

主要部分だけ抜粋します。

lambda.tf
resource "aws_lambda_function" "this" {
  for_each = toset(["a", "b"])

  function_name = "lambda-srcip-${each.key}"
  runtime       = "python3.14"
  # ...

  vpc_config {
    subnet_ids         = aws_subnet.lambda[*].id # 2サブネット
    security_group_ids = [aws_security_group.lambda.id]
  }
}
flow_logs.tf
resource "aws_flow_log" "vpc" {
  vpc_id                   = aws_vpc.this.id
  traffic_type             = "ALL"
  log_destination_type     = "cloud-watch-logs"
  log_destination          = aws_cloudwatch_log_group.flow_logs.arn
  iam_role_arn             = aws_iam_role.flow_logs.arn
  max_aggregation_interval = 60
  log_format               = "$${start} $${end} $${interface-id} $${subnet-id} $${instance-id} $${flow-direction} $${srcaddr} $${srcport} $${dstaddr} $${dstport} $${pkt-srcaddr} $${pkt-dstaddr} $${protocol} $${packets} $${bytes} $${action} $${log-status}"
}

Lambda側は、ソケットで取れる値と、対向サーバーが返してきた「見えた送信元」を一緒に返します。

index.py
def tcp_request_to_receiver():
    with socket.create_connection((RECEIVER_HOST, RECEIVER_PORT), timeout=5) as s:
        local_ip, local_port = s.getsockname()[:2]

        request = f"GET / HTTP/1.1\r\nHost: {RECEIVER_HOST}\r\nConnection: close\r\n\r\n"
        s.sendall(request.encode())

        chunks = []
        while chunk := s.recv(4096):
            chunks.append(chunk)

    _, body = b"".join(chunks).split(b"\r\n\r\n", 1)
    return {
        "socket_getsockname": {"ip": local_ip, "port": local_port},
        "seen_by_receiver": json.loads(body),  # 対向が accept した接続の送信元IP:ポート
    }

では、Lambdaの実際の送信元IPをどう特定すればよいか

方法1:DescribeNetworkInterfacesでHyperplane ENIを事前に調べる

Hyperplane ENIは関数作成時(VPC設定時)に作られるので、呼び出し前に一覧できます。interface-typelambda のENIを絞り込みます。

aws ec2 describe-network-interfaces \
  --filters Name=vpc-id,Values=vpc-xxxxxxxx Name=interface-type,Values=lambda \
  --query 'NetworkInterfaces[].{Id:NetworkInterfaceId,Ip:PrivateIpAddress,Subnet:SubnetId,Desc:Description}' \
  --output table

再現環境の結果です。

Description ENI IP サブネット
AWS Lambda VPC ENI-lambda-srcip-a eni-0a62... 10.0.1.106 10.0.1.0/24
AWS Lambda VPC ENI-lambda-srcip-a eni-0f53... 10.0.1.177 10.0.1.0/24
AWS Lambda VPC ENI-lambda-srcip-a eni-0617... 10.0.2.235 10.0.2.0/24
AWS Lambda VPC ENI-lambda-srcip-a eni-04b3... 10.0.2.80 10.0.2.0/24

ここで分かったことが2つあります。

  • サブネット×SGの組み合わせは2つなのに、ENIは4つ(サブネットあたり2つ) 作られていた。候補を1つに絞れない
  • 2つの関数で共有されているが、Descriptionはすべて最初に作った lambda-srcip-alambda-srcip-b の名前で検索しても見つからない

Hyperplane ENIの数や割り当ては実装の詳細です。ドキュメントにも「負荷分散やヘルスチェックのためにENIを削除・再作成することがある」「14日間アイドルだとENIが回収される」「ENIが存続し続ける前提で設計しないこと」と書かれています。事前に調べたIPはあくまで「候補」として扱うのが安全です。

方法2:フローログ側から逆引きする

送信元IPが分からなくても、宛先IP・宛先ポート は自分で決めているので分かります。そこからフローログを逆引きし、送信元IPを確認します。

CloudWatch Logs Insights の例です(カスタムフォーマットは自動パースされないので parse しています)。

parse @message "* * * * * * * * * * * * * * * * *" as start, end, interface_id, subnet_id, instance_id, flow_direction, srcaddr, srcport, dstaddr, dstport, pkt_srcaddr, pkt_dstaddr, protocol, packets, bytes, action, log_status
| filter dstport = "8080" and flow_direction = "egress" and instance_id = "-"
| sort start desc
| display fromMillis(start * 1000) as start_time, interface_id, srcaddr, srcport, dstaddr, dstport, bytes, action
  • instance-id- のレコードがHyperplane ENI側(EC2のENIなら i-xxxx が入る)
  • 見つかった interface-id を方法1の一覧と照合すれば、どのHyperplane ENIを通ったかも分かる

時刻だけで特定するのは難しい です。今回、Lambda側で記録した送信時刻と、フローログの start のずれは次のとおりでした。

  • Hyperplane ENI側:−16秒〜+31秒(送信より の時刻が入ることもある)
  • 対向EC2のENI側:+2秒〜+59秒

ドキュメントでも start は「パケット受信の最大60秒前〜送信の最大60秒後」になりうると説明されています。記録の順番も送信順と一致しませんでした。
時刻は大まかな絞り込みにとどめ、宛先・ポート・バイト数などを組み合わせて特定するのが確実です。

方法3:接続先リソース側のログで確認する

受信側で accept した接続の送信元を記録する方法です。今回は対向EC2のHTTPサーバーが見えた送信元を返すようにしたので、Lambdaのレスポンスで直接確認できました。

受信側で見えた送信元IP:ポートは、フローログの srcaddr / srcportポート番号まで完全に一致 しました。対向リソースのログを見られる場合は、これが一番確実です。

今回の検証でワークした方法

方法 結果 コメント
ソケット(getsockname() 等) どの実行環境でも 169.254.100.6。フローログには一切出ない
方法1:DescribeNetworkInterfaces 候補(4つ)は出せるが、どれを通るかは実行するまで不明
方法2:フローログから逆引き 宛先IP・ポートで特定できた。時刻は数十秒ずれる
方法3:接続先側のログ フローログとポートまで一致。受信側を触れるなら最も確実

検証結果まとめ

lambda-srcip-alambda-srcip-blambda-srcip-alambda-srcip-b の順に計4回実行し、1回あたり3接続、計12接続を記録しました。

実行ごとの比較

# 関数 実行環境(ログストリーム) ソケット getsockname() のIP 対向・フローログから見えた送信元IP Hyperplane ENI
1 a 5d8f81ad... 169.254.100.6 10.0.1.106 eni-0a62...
2 b 73df8c9f... 169.254.100.6 10.0.2.235 eni-0617...
3 a 65a3c63d...(#1とは別環境) 169.254.100.6 10.0.1.106 eni-0a62...
4 b 73df8c9f...(#2と同じ環境) 169.254.100.6 10.0.2.235 eni-0617...

接続ごとの比較

# 送信時刻(Lambda) 関数 ソケットのポート 対向が見た送信元 フローログ start(Hyperplane ENI egress) フローログ start(対向EC2 ingress)
1 13:33:07 a 52910 10.0.1.106:53526 13:32:53 13:33:15
2 13:33:08 a 52922 10.0.1.106:30812 13:32:59 13:33:10
3 13:33:09 a 52938 10.0.1.106:45110 13:32:53 13:33:15
4 13:38:40 b 45842 10.0.2.235:12606 13:39:01 13:39:19
5 13:38:41 b 45844 10.0.2.235:13699 13:38:41 13:39:19
6 13:38:42 b 45856 10.0.2.235:35513 13:38:56 13:38:44
7 13:40:44 a 53390 10.0.1.106:44606 13:40:58 13:41:05
8 13:40:45 a 53406 10.0.1.106:19328 13:40:58 13:41:44
9 13:40:46 a 46238 10.0.1.106:30205 13:41:17 13:41:05
10 13:40:56 b 57638 10.0.2.235:62947 13:40:56 13:41:44
11 13:40:57 b 57644 10.0.2.235:46997 13:40:59 13:41:18
12 13:40:58 b 57658 10.0.2.235:49301 13:40:56 13:41:44

※ 時刻はすべてUTC。「対向が見た送信元」とフローログの srcaddr:srcport は12接続すべてで一致。

分かったこと

  • ソケットのIPは、関数が違っても実行環境が違っても常に 169.254.100.6。フローログとの突合せどころか、送信元の識別にも使えない
  • 送信元ポートも変換される。Lambda側のポートはほぼ連番なのに、変換後はランダム。ポート番号での突合せもできない
  • Hyperplane ENIは関数間で共有されるlambda-srcip-b は Description が lambda-srcip-a のENIから通信していた
  • フローログの srcaddrpkt-srcaddr は同じ値(Hyperplane ENIのIP)。169.254系のアドレスはフローログのどのフィールドにも現れない
  • 今回の範囲では関数ごとに同じENIが使われたが、4回・12接続だけの観測 なので、「関数ごとにENIが固定される」とまでは言えない

実務への教訓

  • Lambda(VPC)からの通信をフローログで追跡したい場合、ソケットのローカルIPをあてにしてはいけない
    • VPC側から見える送信元は Hyperplane ENI のIPで、ソケットからは取得できない
  • 追跡するなら、トラフィックの特徴(宛先IP・宛先ポート・バイト数)で探す
    • 事前に Hyperplane ENI のIP一覧を把握しておくと、候補の照合に使える
    • 時刻は数十秒単位でずれるので補助的に使う
  • 受信側のログが見られるなら、それが一番確実
  • IP制限のある連携先でも同様の注意が必要
    • TGW / VPCピアリング / Direct Connect などプライベート経路の先で送信元IP制限をかける場合、相手から見えるのは Hyperplane ENI のIP
    • ENIは複数あり、再作成でIPが変わることもあるので、個別IPではなくLambdaを配置したサブネットのCIDR単位で許可する(同一VPC内ならSG参照)のが現実的
    • インターネット向けの場合は、さらにその先の NAT Gateway のEIPが送信元になる

まとめ

  • VPCアタッチしたLambdaの実行環境は、AWS管理のLambda用VPCで動いている
  • ユーザーのVPCに見えるのは、サブネット×SG単位で共有される Hyperplane ENI で、実行環境からの通信はここでNATされる
  • そのため getsockname() などで取れる送信元IP(169.254.x.x)は、フローログや対向リソースから見える送信元IP(Hyperplane ENIのIP)とは一致しない。ポートも変換される
  • Lambdaの送信元を特定したいときは、フローログを宛先・ポートから逆引きするか、受信側のログで確認する

参考文献

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?