用OpenFlow、Ruby、Trema和Open vSwitch实现了ズンドコキヨシ变换代理
受到ZUNDOKO协议的启发,我决定乘上这股大浪,使用OpenFlow创建了ZUNDOKO继承代理。
最近在中国非常流行的“唢呐(Zundoko)祈愿文”源自下面的推文,已经在多种语言中实现并整理成了唢呐祈愿文概览。原本以为只在编程领域流行,但最近在网络上也出现了利用Pcap4J实现唢呐祈愿文协议的案例。
因为Java的课程考试要求“创建并描述一个自定义函数”,所以我写了一个函数,它会持续随机输出“ズン”或“ドコ”,直到输出了“ズン”、“ズン”、“ズン”、“ズン”、“ドコ”这个数组,然后输出“キ・ヨ・シ!”并结束。这个函数获得了满分并拿到了学分。
利用网络技能,为了提升最近开始学习的OpenFlow技能,我在OpenFlow控制器Trema中尝试实现了一个转换代理,并在这个浪潮中试着跟上了进展。
「ズンドコキヨシ変換プロキシ」的含义是将输入的内容转换为类似于日本传统音乐游戏中的”ズンドコ”音频组合的代理工具。
当在TCP通信的数据流中以UTF-8编码找到字符串”ズンズンズンズンズンドコ”时,将其转换为”ズンズンズンズンズンドコ キ・ヨ・シ!”(即进行ズンドコキヨシ转换)。
我們這次使用了ECHO協議(RFC 862)來驗證Zundoko Kiyoshi轉換代理的運作。
按照下面的图示,使用OpenFlow将“ECHO客户端”和“ECHO服务器”的TCP通信转发到“ZUNDOKO TCP代理”。在“ZUNDOKO TCP代理”中,它代理与“ECHO服务器”的TCP通信,并将接收到的数据进行“Zundoko Kiyoshi”转换,然后发送回“ECHO客户端”。

进行
按照下图所示,通过”OpenFlow交换机”对目标地址端口进行转换(目标NATP转换),将目标扭曲为”TCP代理”。
当”OpenFlow控制器”收到”TCP SYN”的PACKET_IN时,会使用FLOW_MOD进行注册,以进行正向流和反向流的目标NAPT转换,并将原始数据包发送为PACKET_OUT。在注册流之后,由于所有操作都由”OpenFlow交换机”完成,因此不会再产生PACKET_IN。

NAPT的问题之一是宛先问题。
普通的情况下进行NAPT转换时,原始目标地址将会丢失,导致在TCP代理中无法确定目标地址。而在HTTP代理中,由于目标信息存在于HTTP请求头中,因此可以通过解析请求头来确定目标。然而,对于通用的TCP代理,由于缺乏用于解析的头部信息,无法确定目标。
NAPT的解决方案是什么?
为了解决上述问题,我们将实施”目标缓存”。我们会将未经NAPT转换的目标信息注册到”目标缓存”中,并在TCP代理中使用它来确定目标。
“OpenFlow控制器”可以了解转换之前的目标IP地址和端口号。我们会将”源IP地址和端口号”作为”目标缓存”的键,并注册”目标IP地址和端口号”。
TCP代理将参考”目标缓存”来确定目标,并作为通用TCP代理访问目标服务器。
环境
以下是我主要使用的软件和库。
-
- OpenFlow 1.0
コントローラ: Trema 0.10.1
スイッチ: Open vSwitch 2.0.2
Ubuntu 14.04
Memcached 1.4.14:宛先キャッシュ
xinetd:echoサーバ
Ruby 2.2.4
trema 0.10.1 : OpenFlowコントローラ
memcache-client 1.8.5 : 宛先キャッシュ アクセス用
我在VirtualBox上设置了四个运行Ubuntu的终端,并构建了环境。我将网络分割为内部系统和管理系统。
-
- u01ホスト
ECHOクライアント:zecho_client.rbで実装
u02ホスト
ECHOサーバ:xinetdでTCP ECHOサーバを動作
u03ホスト
ズンドコキヨシ変換プロキシ:zecho_proxy.rbで実装
ovs1ホスト
OpenFlowコントローラ(Trema):zecho_trema.rbで実装
OpenFlowスイッチ(Open vSwitch):OVSでu01ホストと内部ネットワークをL2ブリッジ
宛先キャッシュ(memcachedサーバ)

ovs1 主机
ovs1ホストでは、下記のようにOpenFlowスイッチの設定します。OVSのスイッチとしてofs0を定義し、ポートと物理ポートを加えます。ローカルホストのOpenFlowのコントローラを指定しています。
sudo ovs-vsctl init
sudo ovs-vsctl add-br ofs0
sudo ifconfig eth1 up
sudo ifconfig eth2 up
sudo ovs-vsctl add-port ofs0 eth1
sudo ovs-vsctl add-port ofs0 eth2
sudo ovs-vsctl set bridge ofs0 protocols=OpenFlow10
sudo ovs-vsctl set-controller ofs0 tcp:127.0.0.1:6653
设置结果如下所示。
$ sudo ovs-vsctl show
ce3126fd-82dd-4f67-abdd-a19aa23ef05e
Bridge "ofs0"
Controller "tcp:127.0.0.1:6653"
is_connected: true
Port "eth2"
Interface "eth2"
Port "ofs0"
Interface "ofs0"
type: internal
Port "eth1"
Interface "eth1"
ovs_version: "2.0.2"
源代码
编程源代码
ECHO客户端
以下是一个简单的脚本,它会向TCP ECHO端口(TCP7号)发送随机生成的“ズン”和“ドコ”字符串,并显示接收结果。
require 'socket'
socket = TCPSocket.open('172.16.0.2', 7)
words = %w(ズン ドコ)
10.times do
string = Array.new(5) { words.sample }.join
puts "SEND: #{string}"
socket.puts string
puts "RECV: #{socket.gets}"
puts
end
打开流控制器
switch_readyメソッド:
今回はu01ホストから送信されたパケットのみを処理対象とするため、u01以外のポートからの通信とブロードキャスト通信用の通信フローを設定しています。
packet_inメソッド:
u01ホストから受信したパケットを、NAPT変換対象かどうかsend_nat_packet_and_add_nat_flowで振り分けて、NAPT変換対象外だった場合は、通常通りPACKET_OUTします。
send_nat_packet_and_add_nat_flowメソッド:
NAPT変換対象のTCPパケットだった場合には、NAPT変換用の順方向フロート逆方向フローをOpenFlowスイッチに設定します。併せて「宛先キャッシュ」に宛先情報を設定します。その後PACKET_OUTします。
require 'memcache'
MEMCACHE = MemCache.new '192.168.88.4:11211'
PROXY_L1_PORT = 2
PROXY_MAC = '08:00:27:b6:63:f2'.freeze
PROXY_IP = '172.16.0.3/32'.freeze
PROXY_TCP_PORT = 10000
INTERNAL_PORT = 1
EXTERNAL_PORT = 2
IP_PROTO_TCP = 6
# ZEcho Trema
class ZechoTrema < Trema::Controller
def start(_args)
logger.info "#{name} started."
end
def switch_ready(datapath_id)
logger.info "switch_ready: #{datapath_id}"
send_flow_mod_add(
datapath_id,
match: Match.new(in_port: EXTERNAL_PORT),
actions: SendOutPort.new(INTERNAL_PORT)
)
send_flow_mod_add(
datapath_id,
match: Match.new(destination_mac_address: 'FF:FF:FF:FF:FF:FF'),
actions: SendOutPort.new(:flood)
)
end
def switch_disconnected(datapath_id)
logger.info "switch_disconnected: #{datapath_id}"
end
def packet_in(datapath_id, message)
packet_send = send_nat_packet_and_add_nat_flow(datapath_id, message)
unless packet_send
send_packet_out(
datapath_id,
packet_in: message,
actions: SendOutPort.new(EXTERNAL_PORT)
)
end
end
private
def send_nat_packet_and_add_nat_flow(datapath_id, message)
return false unless message.in_port == INTERNAL_PORT
return false unless Pio::Parser::IPv4Packet == message.data.class
return false unless message.ip_protocol == IP_PROTO_TCP
src_mac = message.source_mac
src_ip = message.source_ip_address
src_port = message.transport_source_port
dst_mac = message.destination_mac
dst_ip = message.destination_ip_address
dst_port = message.transport_destination_port
MEMCACHE.set "#{src_ip}:#{src_port}", "#{dst_ip}:#{dst_port}"
logger.info "NAT #{src_ip}:#{src_port} => #{dst_ip}:#{dst_port} via #{PROXY_IP}:#{PROXY_TCP_PORT}"
# forward rule
forward_match = ExactMatch.new(message)
forward_actions = [
Pio::OpenFlow10::SetDestinationMacAddress.new(PROXY_MAC),
Pio::OpenFlow10::SetDestinationIpAddress.new(PROXY_IP),
Pio::OpenFlow10::SetTransportDestinationPort.new(PROXY_TCP_PORT),
SendOutPort.new(PROXY_L1_PORT)
]
send_flow_mod_add(
datapath_id,
priority: 1000,
idle_timeout: 300,
match: forward_match,
actions: forward_actions
)
# reverse rule
reverse_match = ExactMatch.new(message)
reverse_match.in_port = PROXY_L1_PORT
reverse_match.source_mac_address = PROXY_MAC
reverse_match.destination_mac_address = src_mac
reverse_match.source_ip_address = PROXY_IP
reverse_match.destination_ip_address = src_ip
reverse_match.transport_source_port = PROXY_TCP_PORT
reverse_match.transport_destination_port = src_port
reverse_actions = [
Pio::OpenFlow10::SetSourceMacAddress.new(dst_mac),
Pio::OpenFlow10::SetSourceIpAddress.new(dst_ip),
Pio::OpenFlow10::SetTransportSourcePort.new(dst_port),
SendOutPort.new(message.in_port)
]
send_flow_mod_add(
datapath_id,
priority: 1000,
idle_timeout: 300,
match: reverse_match,
actions: reverse_actions
)
send_packet_out(
datapath_id,
packet_in: message,
actions: forward_actions
)
return true
rescue NotImplementedError => e
logger.error "#{e}: #{message.data}"
end
false
end
ズンドコキヨシ変換プロキシ
这个ZechoProxyServer类会在代理端口上进行监听,并且由ZechoProxyClient类动态生成,每当有连接请求时。
ZechoProxyServer类:
该类监听PROXY_TCP_PORT端口,当接收到来自客户端的连接请求时,会生成一个ZechoProxyClient类的实例。这是一个简单的实现。
ZechoProxyClient类:
根据发送方的IP地址和端口号,查阅”目标缓存”,获取连接的目标IP地址和端口号。然后,通过TCPSocket.new与目标建立连接。
生成用于发送和接收的线程,将从服务器接收的数据传递给zundoko方法,执行”ズンドコキヨシ”转换,并将其发送给客户端。
require 'socket'
require 'memcache'
PROXY_TCP_PORT = 10_000
MEMCACHE = MemCache.new '192.168.88.4:11211'
ZUNDOKO_REGEXP = /((ズン\s*){4}ドコ)/
IPPort = Struct.new(:ip, :port)
Thread.abort_on_exception = true
# ZEchoProxy
class ZechoProxyClient
def initialize(server_socket)
@threads = []
@server_socket = server_socket
@client_socket, @src, @dst = lookup_nat_table(server_socket)
end
def run
puts "#{self}: start proxy client"
return unless @client_socket
@threads << Thread.new do
begin
while data = @server_socket.recv(65535)
break if data.size == 0
@client_socket.write(data)
@client_socket.flush
end
rescue IOError
ensure
close
end
end
@threads << Thread.new do
begin
while data = @client_socket.recv(65535)
break if data.size == 0
data = zundoko(data)
@server_socket.write(data)
@server_socket.flush
end
rescue IOError
ensure
close
end
end
end
def close
@client_socket.close unless @client_socket.closed?
@server_socket.close unless @server_socket.closed?
end
def join
@threads.map { |t| t.join if t }
end
def to_s
"#{@src.ip}:#{@src.port} => #{@dst.ip}:#{@dst.port}"
end
private
def zundoko(data)
begin
encoded_data = data.encode('UTF-8', 'UTF-8', invalid: :replace, undef: :replace)
if ZUNDOKO_REGEXP === encoded_data
data = encoded_data.gsub(ZUNDOKO_REGEXP) { "#{$1} キ・ヨ・シ!" }
end
rescue ArgumentError
rescue => e
puts "#{e.class} #{e}"
end
data
end
def lookup_nat_table(server_socket)
_inet, src_port, _src_host, src_ip = server_socket.peeraddr
client_socket = nil
src = IPPort.new(src_ip, src_port.to_s)
dst = IPPort.new
ip_port_key = "#{src.ip}:#{src.port}"
10.times do
ip_port = MEMCACHE.get(ip_port_key)
if ip_port
MEMCACHE.delete(ip_port_key)
dst.ip, dst.port = ip_port.split(/:/)
client_socket = TCPSocket.new(dst.ip, dst.port)
break
end
sleep 0.5
end
[client_socket, src, dst]
end
end
# ZEchoProxyServer
class ZechoProxyServer
def initialize(port = PROXY_TCP_PORT)
@threads = []
@port = port
end
def run
puts "proxy server start: port:#{@port}"
@threads << Thread.new do
tcp_server = TCPServer.open(@port)
loop do
socket = tcp_server.accept
client = ZechoProxyClient.new(socket)
client.run
end
end
end
def join
@threads.map { |t| t.join if t }
end
end
if __FILE__ == $0
server = ZechoProxyServer.new
server.run
server.join
end
执行结果
在ECHO客户端的控制台界面上,ECHO客户端会随机发送”ズンドコ”给ECHO服务器,如果条件匹配,会额外显示”キ・ヨ・シ!”。

附录
今回は汎用的なTCPプロキシを実装したため、通常のWebブラウザからの通信もズンドコキヨシ変換できます。
u01ホストでWebブラウザを起動し、u02ホストで下記のWebサーバにアクセスした実行結果は下記のとおりになります。
require 'sinatra'
set bind: '0.0.0.0'
get '/' do
erb :index
end
__END__
@@ index
<% words = %w(ズン ドコ) %>
<html><head></head><body><ul>
<% 100.times do %>
<li><%= Array.new(5) { words.sample }.join %></li>
<% end %>
</ul></body></html>

実際のインターネット上でも試してみましたが、メジャーなサイトはHTTPSのため、残念ながらズンドコキヨシ変換をかけることはできませんでした。このため、ローカルで簡易なWebサーバを立ち上げて実行しています。
请你提供一种中文的同义改写。
[増補改訂版]クラウド時代のネットワーク技術 OpenFlow実践入門
@qb0c80aEさん提供ありがとうございました!
TremaでOpenFlowプログラミング
ズンドコキヨシまとめ