
在上一篇文章里，我们把范围控制在 OVN 的纯内部逻辑网络：逻辑交换机、逻辑路由器、ACL 和 Port Security。这一篇继续往外走一步，讨论一个更接近真实云网络的问题：

- 租户网络如何访问外部网络？
- 外部网络如何访问内部实例？
- OVN 里的 `gateway router`、`localnet`、`SNAT`、`DNAT` 到底是什么关系？

先把核心关系列清楚：

- `localnet` 用来把 OVN 的逻辑交换机接到宿主机可达的物理二层网络；
- `gateway router` 是 OVN 里承接南北向流量的逻辑路由器角色；
- `SNAT` 解决“内部出去”；
- `DNAT` 解决“外部进来”；
- 更常见的“给内部实例分配一个对外 IP”通常会用 `dnat_and_snat`。

---

## 1. 核心概念

### localnet

`localnet` 是一种逻辑交换机端口类型，用来把逻辑交换机接到本机 OVS 上的 provider bridge。它不是普通 VM 端口，也不走 Geneve 隧道，而是直接把某个逻辑二层网络映射到宿主机可达的物理二层网络。

典型配置长这样：

```bash
ovn-nbctl lsp-add ls-public lsp-public-localnet
ovn-nbctl lsp-set-type lsp-public-localnet localnet
ovn-nbctl lsp-set-addresses lsp-public-localnet unknown
ovn-nbctl lsp-set-options lsp-public-localnet network_name=provider
```

与此同时，本机 OVS 需要配置：

```bash
ovs-vsctl set open . external-ids:ovn-bridge-mappings=provider:br-provider
```

这里的意思是：OVN 里名为 `provider` 的 `localnet`，映射到本机的 `br-provider`。

注意：

- 这条命令会直接写入 `external-ids:ovn-bridge-mappings`
- 如果你的环境里已经有别的 bridge mapping，直接执行可能会把原值覆盖掉
- 在共享环境或已有配置的机器上，应该先查看现有值，再决定是追加还是替换

### gateway router

`gateway router` 底层仍然是 `Logical_Router`，但它不只是一个宽泛的“逻辑角色”描述。在 OVN 架构里，更准确的说法是：当 `Logical_Router.options:chassis` 被设置为某个 chassis 的名字或 UUID 时，这台逻辑路由器会以中心化方式运行在该 chassis 上，承担南北向流量和 NAT 出入口职责。

可以把它和未设置 `options:chassis` 的分布式路由器对比理解：

- 设置了 `options:chassis`：更接近中心化 gateway router，相关流量在指定 chassis 上集中处理
- 未设置 `options:chassis`：更接近分布式路由语义，转发和部分状态处理会分散到各个相关 chassis

下面这套实验是单机环境，只有一个 chassis，因此即使不显式设置 `options:chassis`，有些场景也可能跑通。但在有多个 chassis 的环境里，最好显式设置。

后面的实验步骤会直接把 `lr-edge` 绑定到当前 chassis。

在最小拓扑里，它通常有两侧：

- 一侧连租户内部网络，比如 `10.0.1.0/24`
- 一侧连 provider/public 网络，比如 `192.168.2.0/24`

### SNAT、DNAT、dnat_and_snat

OVN 支持 3 种常用 NAT 类型：

| 类型            | 作用                                                |
| :-------------- | :-------------------------------------------------- |
| `snat`          | 内部地址访问外部时，把源地址改成外部地址            |
| `dnat`          | 外部访问某个地址时，把目标地址改成内部地址          |
| `dnat_and_snat` | 同时做入站 DNAT 和出站 SNAT，常用来实现 floating IP |

官方命令是：

```bash
ovn-nbctl lr-nat-add <router> snat <external_ip> <logical_ip_or_cidr>
ovn-nbctl lr-nat-add <router> dnat <external_ip> <logical_ip>
ovn-nbctl lr-nat-add <router> dnat_and_snat <external_ip> <logical_ip> [<logical_port> <external_mac>]
```

其中：

- 单机实验或中心化部署里，常见写法就是前 4 个参数
- 在多节点分布式场景下，`dnat_and_snat` 可以额外带上 `logical_port` 和 `external_mac`
- 当显式指定这两个参数时，floating IP 的 ARP 应答和相关流量可以更靠近实例所在 chassis 处理

---

## 2. 实验目标

这一篇不把实验拆成多个主题，而是围绕一套完整拓扑验证 3 件事：

1. 内部实例通过 `SNAT` 访问外部网络
2. 外部客户端通过 `dnat_and_snat` 访问内部实例
3. 理解 `localnet + provider bridge + gateway router` 的协作关系

实验边界：

- 这套实验是单机环境，用 namespace 模拟内部实例和外部网络；
- 实验关注的是概念闭环，不是高可用、BFD、双网关等生产级细节；
- 本文不再混入 ACL、LB、Port Security 等其他主题。

---

## 3. 实验拓扑

```mermaid
flowchart LR
    subgraph PRIVATE["Logical Switch ls-private (10.0.1.0/24)"]
        VM1["vm1 / lsp-vm1\n10.0.1.10"]
        LSP1["ls-private-to-lr\n(type=router)"]
    end

    subgraph ROUTER["Logical Router lr-edge"]
        LRP1["lrp-private\n10.0.1.1/24"]
        LRP2["lrp-public\n192.168.2.150/24"]
    end

    subgraph PUBLIC["Logical Switch ls-public (192.168.2.0/24)"]
        LSP2["ls-public-to-lr\n(type=router)"]
        LOCALNET["lsp-public-localnet\nlocalnet=provider"]
    end

    subgraph HOST["Host OVS"]
        BR["br-provider"]
    end

    subgraph EXT["Namespace ext"]
        EXTNS["ext\n192.168.2.151"]
    end

    VM1 --- LSP1
    LSP1 --- LRP1
    LRP1 --- LRP2
    LRP2 --- LSP2
    LOCALNET --- BR
    BR --- EXTNS

    style PRIVATE fill:#4a90d9,color:#fff
    style ROUTER fill:#27ae60,color:#fff
    style PUBLIC fill:#e67e22,color:#fff
    style BR fill:#8e44ad,color:#fff
```

地址规划：

- 私有网络：`10.0.1.0/24`
- 公网/外部网络：`192.168.2.0/24`
- `vm1`：`10.0.1.10`
- `ext`：`192.168.2.151`
- `lr-edge` 内网侧网关：`10.0.1.1`
- `lr-edge` 外网侧地址：`192.168.2.150`
- 对外暴露的 floating IP：`192.168.2.152`

说明：

- 这里使用 `192.168.2.150-152` 作为示例地址
- 在别的环境里复现实验时，应替换成所在二层网络里未被占用的地址

---

## 4. 创建实验环境

### 创建 provider bridge 和外部 namespace

```bash
# provider bridge，用来承接 localnet 映射
ovs-vsctl --may-exist add-br br-provider

# 告诉 OVN：network_name=provider 对应 br-provider
# 注意：如果当前已有 ovn-bridge-mappings，这条命令会覆盖原值
ovs-vsctl set open . external-ids:ovn-bridge-mappings=provider:br-provider

# 创建外部 namespace
ip netns add ext
ip link add veth-ext type veth peer name br-ext
ip link set veth-ext netns ext

ip netns exec ext ip addr replace 192.168.2.151/24 dev veth-ext
ip netns exec ext ip link set veth-ext up
ip netns exec ext ip link set lo up

ip link set br-ext up
ovs-vsctl --may-exist add-port br-provider br-ext
```

### 创建逻辑网络、路由器和 localnet

```bash
ovn-nbctl ls-add ls-private
ovn-nbctl ls-add ls-public
ovn-nbctl lr-add lr-edge

# 将 lr-edge 显式设为当前 chassis 上的 gateway router
CHASSIS=$(ovn-sbctl --bare --columns=name find Chassis hostname=$(hostname -f))
ovn-nbctl set Logical_Router lr-edge options:chassis="$CHASSIS"

# 私有网络中的 VM 端口
ovn-nbctl lsp-add ls-private lsp-vm1
ovn-nbctl lsp-set-addresses lsp-vm1 "02:ac:10:ff:01:10 10.0.1.10"

# 逻辑路由器端口
ovn-nbctl lrp-add lr-edge lrp-private 02:ac:10:ff:01:01 10.0.1.1/24
ovn-nbctl lrp-add lr-edge lrp-public 02:ac:10:ff:02:01 192.168.2.150/24

# ls-private -> lr-edge
ovn-nbctl lsp-add ls-private ls-private-to-lr
ovn-nbctl lsp-set-type ls-private-to-lr router
ovn-nbctl lsp-set-addresses ls-private-to-lr router
ovn-nbctl lsp-set-options ls-private-to-lr router-port=lrp-private

# ls-public -> lr-edge
ovn-nbctl lsp-add ls-public ls-public-to-lr
ovn-nbctl lsp-set-type ls-public-to-lr router
ovn-nbctl lsp-set-addresses ls-public-to-lr router
ovn-nbctl lsp-set-options ls-public-to-lr router-port=lrp-public
ovn-nbctl set Logical_Switch_Port ls-public-to-lr options:nat-addresses=router

# ls-public -> provider bridge
ovn-nbctl lsp-add ls-public lsp-public-localnet
ovn-nbctl lsp-set-type lsp-public-localnet localnet
ovn-nbctl lsp-set-addresses lsp-public-localnet unknown
ovn-nbctl lsp-set-options lsp-public-localnet network_name=provider
```

### 创建内部实例 namespace

```bash
ip netns add vm1
ip link add veth-vm1 type veth peer name ovs-vm1
ip link set veth-vm1 netns vm1

ip netns exec vm1 ip link set veth-vm1 address 02:ac:10:ff:01:10
ip netns exec vm1 ip addr replace 10.0.1.10/24 dev veth-vm1
ip netns exec vm1 ip link set veth-vm1 up
ip netns exec vm1 ip link set lo up
ip netns exec vm1 ip route replace default via 10.0.1.1 dev veth-vm1

ip link set ovs-vm1 up
ovs-vsctl --may-exist add-port br-int ovs-vm1
ovs-vsctl set interface ovs-vm1 external_ids:iface-id=lsp-vm1
```

### 基础连通性检查

```bash
ovn-sbctl show
ovs-vsctl show

# vm1 应该能 ping 到自己的逻辑网关
ip netns exec vm1 ping -c2 10.0.1.1

# ext 应该能 ping 到公网侧逻辑网关
ip netns exec ext ping -c2 192.168.2.150

# 检查公网侧 peer LSP 是否已启用 NAT 地址同步
ovn-nbctl get Logical_Switch_Port ls-public-to-lr options
```

---

## 5. 实验一：SNAT

目标：让 `vm1` 访问 `ext` 时，源地址从 `10.0.1.10` 变成 `192.168.2.150`。

### 配置 SNAT

```bash
ovn-nbctl lr-nat-add lr-edge snat 192.168.2.150 10.0.1.0/24
ovn-nbctl lr-nat-list lr-edge
```

### 验证

在 `ext` 里启动一个简单 HTTP 服务：

```bash
ip netns exec ext sh -c \
  'python3 -m http.server 8080 --bind 192.168.2.151 >/tmp/ovn-ext-http.log 2>&1 &'
```

然后从 `vm1` 访问：

```bash
ip netns exec vm1 curl -s 192.168.2.151:8080 >/dev/null
```

如果需要观察源地址，可以在 `ext` 里抓包：

```bash
ip netns exec ext tcpdump -ni veth-ext tcp port 8080
```

预期现象：

- `vm1` 能访问 `192.168.2.151:8080`
- `ext` 看到的源地址应是 `192.168.2.150`，而不是 `10.0.1.10`

---

## 6. 实验二：DNAT / Floating IP

纯 `dnat` 适合说明“外部访问某个地址时，目标地址被改到内部实例”；但在 VM/云网络实践里，更常见的是 `dnat_and_snat`，也就是 floating IP。

这里用 `dnat_and_snat` 做入站验证。

补充说明：

- 这里采用单机环境里更直观的最短命令
- 在多节点分布式场景下，如果希望 floating IP 的处理更靠近实例所在 chassis，可以显式补上 `logical_port` 和 `external_mac`
- 这一节默认你已经完成前面创建阶段里的 `options:chassis` 和 `options:nat-addresses=router` 配置，否则外部地址可能无法被正确通告和访问

### 在 vm1 上启动服务

```bash
ip netns exec vm1 sh -c \
  'python3 -m http.server 8080 --bind 10.0.1.10 >/tmp/ovn-vm1-http.log 2>&1 &'
```

### 配置 floating IP

```bash
ovn-nbctl lr-nat-add lr-edge dnat_and_snat 192.168.2.152 10.0.1.10
ovn-nbctl lr-nat-list lr-edge
```

多节点分布式场景下，常见写法会进一步写成：

```bash
ovn-nbctl lr-nat-add lr-edge dnat_and_snat 192.168.2.152 10.0.1.10 lsp-vm1 02:ac:10:ff:01:10
```

这不是这里这套单机实验的必需项，但在多节点环境里需要知道这层差异。

### 验证

从 `ext` 访问这个 floating IP：

```bash
ovn-nbctl show lr-edge
ovn-nbctl get Logical_Switch_Port ls-public-to-lr options
ip netns exec ext curl -s 192.168.2.152:8080
```

预期现象：

- 能返回 `vm1` 上 HTTP 服务的内容
- 从外部看，访问的是 `192.168.2.152`
- 在 OVN 内部，流量被映射到 `10.0.1.10`

---

## 7. 常用排障命令

```bash
# 查看整个逻辑拓扑
ovn-nbctl show

# 查看路由器 NAT 规则
ovn-nbctl lr-nat-list lr-edge

# 查看所有 Southbound 绑定
ovn-sbctl show

# 查看 br-int / br-provider 配置
ovs-vsctl show

# 查看 provider bridge 上的数据面流
ovs-appctl dpif/dump-flows br-provider

# 在 ext 中查看来自内部实例的 NAT 后流量
ip netns exec ext tcpdump -ni veth-ext

# 在 vm1 中查看默认路由
ip netns exec vm1 ip route
```

如果实验不通，优先按这个顺序检查：

1. `ovs-vsctl get open . external-ids` 中是否包含正确的 `ovn-bridge-mappings`
2. `ovn-sbctl show` 中 `lsp-vm1` 是否已绑定
3. `ovn-nbctl show lr-edge` 中是否已设置 `options:chassis`
4. `ovn-nbctl get Logical_Switch_Port ls-public-to-lr options` 中是否包含 `nat-addresses=router`
5. `br-provider` 是否存在，且 `br-ext` 是否已加入
6. `vm1` 和 `ext` 的默认路由/IP 是否正确
7. `lr-nat-list lr-edge` 是否已包含对应 NAT 规则

---

## 8. 清理环境

```bash
# 删除 NAT 规则
# 注意：只给 router 名称时，会删除该逻辑路由器上的全部 NAT 规则
ovn-nbctl lr-nat-del lr-edge

# 删除 OVS 端口
ovs-vsctl --if-exists del-port br-int ovs-vm1
ovs-vsctl --if-exists del-port br-provider br-ext

# 删除 namespace
ip netns del vm1 2>/dev/null || true
ip netns del ext 2>/dev/null || true

# 删除逻辑网络
ovn-nbctl --if-exists lr-del lr-edge
ovn-nbctl --if-exists ls-del ls-private
ovn-nbctl --if-exists ls-del ls-public

# 删除 provider bridge
ovs-vsctl --if-exists del-br br-provider

# 清理临时文件
rm -f /tmp/ovn-ext-http.log /tmp/ovn-vm1-http.log
```

---

## 9. 总结

- `localnet` 负责把逻辑交换机接到真实可达的二层网络
- `gateway router` 负责承接南北向流量
- `SNAT` 解决“内部出去”
- `DNAT` 解决“外部进来”
- `dnat_and_snat` 更像日常说的 floating IP

如果只做纯内部逻辑网络实验，上一篇文章里的交换机、路由器、ACL、Port Security 就够了。  
继续往“外部出口、浮动 IP、公网接入”推进时，OVN 的 gateway 和 NAT 就是下一步要掌握的内容。

---

## 10. 参考

- [OVN NBCTL Manual](https://www.ovn.org/support/dist-docs/ovn-nbctl.8.html)
- [OVN Northbound DB Schema](https://www.ovn.org/support/dist-docs/ovn-nb.5.html)
- [OVN Architecture](https://www.ovn.org/support/dist-docs/ovn-architecture.7.html)

