用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客户端”。

概要.PNG

进行

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

構成.PNG

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サーバ)

環境.PNG

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服务器,如果条件匹配,会额外显示”キ・ヨ・シ!”。

実行結果CLI.png

附录

今回は汎用的な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>
実行結果WEB.png

実際のインターネット上でも試してみましたが、メジャーなサイトはHTTPSのため、残念ながらズンドコキヨシ変換をかけることはできませんでした。このため、ローカルで簡易なWebサーバを立ち上げて実行しています。

请你提供一种中文的同义改写。

[増補改訂版]クラウド時代のネットワーク技術 OpenFlow実践入門

@qb0c80aEさん提供ありがとうございました!

TremaでOpenFlowプログラミング
ズンドコキヨシまとめ

bannerAds