Upgrade to Pro — share decks privately, control downloads, hide ads and more …

関数型言語で始めるネットワークプログラミング

kamijin_fanta
November 11, 2018
670

 関数型言語で始めるネットワークプログラミング

kamijin_fanta

November 11, 2018
Tweet

Transcript

  1. 関数型言語で始める
    ネットワークプログラミング
    2018‑11‑11 ScalaKansaiSummit ‑ kamijin̲fanta

    View Slide

  2. 範囲
    パーサ
    pcap
    dhcp
    完全に詰め込みすぎた
    2

    View Slide

  3. 注意点
    概要を理解してもらうための資料です
    厳密には仕様違反になっている箇所も多いです
    RCFによると~とか言わないでください
    3

    View Slide

  4. repo
    https://github.com/kamijin‑fanta/scala‑udp‑sandbox
    4

    View Slide

  5. パーサコンビネータ
    プログラミング言語等の構文解析などを行うライブラリ
    xml, json, js, etc...
    汎用プログラミング言語で行うことが特徴
    DSLとかが記述しやすい・関数型言語での実装が多い
    js, scala, haskell, ruby
    メリット
    拡張性が高い
    適切な構文エラーが出る
    デメリット
    遅い
    5

    View Slide

  6. パーサコンビネータをScala
    で書く
    静的型付け言語なので、コンパイル時にある程度エラー分かる
    関数型言語に有る代数的データ型の表現が便利
    引数に0個以上の引数を渡し、そのフィールドを持つ
    (後から登場します)
    パターンマッチングが便利
    6

    View Slide

  7. ライブラリの選択肢
    デフォルト
    scala.util.parsing.combinator 正規表現で楽にかける
    ライブラリ
    https://www.lihaoyi.com/fastparse/ 早い・ストリーム・バイト列にも対応
    今回はfastparse使う
    7

    View Slide

  8. 簡単な例
    .! は変数抽出
    !Hoge ~ Fuga は、先読みを行ってHogeが否定マッチすれば、Fugaに進む
    Hoge.rep は繰り返し min,max,sepが指定可能
    val input = "yummy_cookie=choco; tasty_cookie=strawberry"
    val key = (!"=" ~ AnyChar).rep.! // "=" 以外の文字列にマッチ
    val value = (!CharIn(";", " ") ~ AnyChar).rep.!
    val field = P(key ~ "=" ~ value)
    val cookie = P(Start ~ field.rep(sep = "; ") ~ End)
    val res = cookie.parse(input).get.value
    assert(res === Seq(
    "yummy_cookie" ‐> "choco",
    "tasty_cookie" ‐> "strawberry",
    ))
    cookieの俺俺パーサーって人類の8割が書いていると思う
    8

    View Slide

  9. JSON
    を雑にパースする
    指数/少数の数値・文字列エスケープなどを扱わない
    記法 型
    記法 型
    {....} JsObject(member: Map[String, JsValue])
    [...] JsArray(items: List[JsValue])
    1234 1.23 10e3 JsNumber(value: BigDecimal)
    true false JsBoolean(value: Boolean)
    "" JsString(value: String)
    null JsNull
    type Json =
    JsObject | JsArray | JsNumber | JsBoolean | JsString | JsNull
    9

    View Slide

  10. AST
    っぽい感じの型を定義
    ast=abstract syntax tree
    object Json {
    // like: type JsonExpr = JsObject | JsArray | JsNumber | JsBoolean | JsString | JsNull
    sealed trait JsonExpr
    case class JsObject(obj: Map[String, JsonExpr]) extends JsonExpr
    case class JsArray(values: Seq[JsonExpr]) extends JsonExpr
    case class JsBoolean(value: Boolean) extends JsonExpr
    case class JsNumber(value: Int) extends JsonExpr
    case class JsString(value: String) extends JsonExpr
    case object JsNull extends JsonExpr
    }
    10

    View Slide

  11. val json =
    JsArray(List(
    JsObject(Map(
    "id" ‐> JsNumber(1234),
    "name" ‐> JsString("hoge")
    ))
    ))
    [{ "id": 1234, "name": "hoge"}]
    11

    View Slide

  12. object JsonParser {
    val space = CharsWhileIn(" \r\n").rep // 空白のいずれかにマッチ
    val char = (!CharIn("\"\\") ~ AnyChar).! // "\ 以外の文字列にマッチ
    val chars = space ~ "\"" ~ char.rep.! ~ "\"" ~ space // ""に囲まれた文字列
    val digit = CharIn('0' to '9').!
    val bool = P("true").map(_=>JsBoolean(true)) | P("false").map(_=>JsBoolean(false))
    val string = chars.map(s => JsString(s))
    val nul = P("null").map(_ => JsNull)
    val number = P(CharIn("+‐").? ~ digit.rep(min=1)).!.map(v => JsNumber(v.toInt))
    val objPare = P(chars ~/ ":" ~/ json)
    val obj = P("{" ~/ objPare.rep(sep = ",".~/) ~/ "}").map(s => JsObject(s.toMap))
    val array = P("[" ~/ json.rep(sep = ",".~/) ~/ "]").map(s => JsArray(s))
    val json: all.Parser[JsonExpr] =
    P(space ~ (obj | string | array | nul | number | bool) ~ space)
    }
    val input = """ { "text": "value", "array": [null, 1234, "str"]} """
    12

    View Slide

  13. パーサー書くの楽しい!
    世はテキストに溢れている… HTTP・URL・DSL
    デメリットを忘れない(遅い)
    https://www.lihaoyi.com/fastparse/#Performance
    13

    View Slide

  14. バイト列に対してもパーサーは書ける
    fastparseはバイト列に対応してる
    デメリット
    遅い
    だいたい手続きプログラミングでパースしやすいように出来てる
    だいたいstruct使ったほうが楽(C言語最強)
    メリット
    拡張性(?)
    楽しい
    14

    View Slide

  15. 本題
    dhcpパケットをパーサコンビネータに食わす
    ついでにIPを割り当てるサーバ機能も付ける
    15

    View Slide

  16. DHCP
    Dynamic Host Configuration Protocol(ダイナミック ホスト コンフィギュレーション
    プロトコル、DHCP)とは、コンピュータがネットワーク接続する際に必要な情報を自
    動的に割り当てるプロトコルのことをいう。
    DHCP は BOOTP の上位互換であり、メッセージ構造などは変わっていない。DHCP で
    は、BOOTP に比べて自動設定できる情報が増え、より使いやすくなっている。
    https://ja.wikipedia.org/wiki/Dynamic̲Host̲Configuration̲Protocol
    自動設定可能な項目
    IPアドレス・デフォルトゲートウェイ・DNS
    ホスト名
    NTP
    プリンター
    16

    View Slide

  17. DHCP
    プロトコル
    DHCPサーバは、予めアドレスレンジを確保しておく
    (Discovery) ブロードキャストのUDPメッセージでDHCPサーバを探す
    (Offer) DHCPサーバは、アドレス・オプション値などを返す
    (Request) IPが競合していないことを確認してからアドレスの割当依頼を送信
    (Ack) 割当許可
    https://ja.wikipedia.org/wiki/Dynamic̲Host̲Configuration̲Protocol
    17

    View Slide

  18. DHCP
    パケット
    https://www.infraexpert.com/study/tcpip13.html
    UDP サーバ: Port67 クライアント: Port68
    パケット構造は、上り下りで全く同じ (UDPポート・メッセージタイプ・オプション値
    が異なる)
    18

    View Slide

  19. wireshark見るとわかりやすい
    19

    View Slide

  20. http://www.ids‑sax2.com/Unicorn/Tutorials/Dynamic‑Host‑Configuration‑Protocol.htm
    20

    View Slide

  21. DHCP Discover
    21

    View Slide

  22. otpion
    Type(1) Length(1) Data(n)
    Type(1) Length(1) Data(n)
    53 1 01 (discover)
    50 4 c0 a8 6f 69 (192.168.111.105)
    12 8 PC103553
    22

    View Slide

  23. 型定義
    case class BootpPacket(
    bootpType: Short,
    transactionId: Long,
    yourClientIp: Inet4Address,
    clientMac: MacAddress,
    options: Seq[BootpOption])
    case class DhcpOptions(
    messageType: Int,
    mask: Option[String],
    router: Option[String],
    dns: Option[String],
    leaseTime: Option[Int]
    )
    23

    View Slide

  24. ついでに列挙体も定義
    object BootpOptionType extends Enumeration {
    val SubnetMask = 1.toShort
    val Router = 3.toShort
    val DNS = 6.toShort
    val HostName = 12.toShort
    val RequestedIpAddress = 50.toShort
    val IpAddressLeaseTime = 51.toShort
    val DhcpMessageType = 53.toShort
    val DhcpServerIdentify = 54.toShort
    val ParameterRequestList = 55.toShort
    }
    object DhcpMessageType extends Enumeration {
    val Discover = 1
    val Offer = 2
    val Request = 3
    val Ack = 5
    val Release = 7
    }
    24

    View Slide

  25. fastparse
    val parser: core.Parser[BootpPacket, Byte, all.Bytes] = P(
    // bootpType eth addrLen Hops Trans
    UInt8 ~ BS(0x01, 0x06) ~ AnyByte ~ UInt32 ~ AnyBytes(8) ~
    // ip mac address
    AnyBytes(4).! ~ AnyBytes(8) ~ UInt8.rep(exactly = 6) ~ AnyBytes(10) ~
    // ServerHostName BootFileName Magic Cookie
    AnyBytes(64) ~ AnyBytes(128) ~ AnyBytes(4) ~
    // options type(1) length(1) value(n)
    (Int8.filter(_ != ‐1) ~ Int8.flatMap(size => AnyBytes(size).!))
    .map(x => BootpOption(x._1, x._2))
    .rep ~ BS(0xff) // option end
    ).map(x => {
    BootpPacket(
    bootpType = x._1, transactionId = x._2,
    yourClientIp = ByteArrays.getInet4Address(x._3.toArray, 0),
    clientMac = MacAddress.getByAddress(x._4.map(_.toByte).toArray), options = x._5
    )
    })
    25

    View Slide

  26. パケット生成
    case class BootpPacket(
    bootpType: Short,
    transactionId: Long,
    yourClientIp: Inet4Address,
    clientMac: MacAddress,
    options: Seq[BootpOption]) {
    def asBytes: Bytes =
    Bytes(bootpType) ++ Bytes(0x01, 0x06, 0) ++
    Bytes.fromLong(transactionId, size = 4) ++ Bytes.fill(8)(0) ++
    Bytes(yourClientIp.getAddress) ++ Bytes.fill(8)(0) ++
    Bytes(clientMac.getAddress) ++ Bytes.fill(10)(0) ++
    Bytes.fill(64)(0) ++ Bytes.fill(128)(0) ++ Bytes(0x63, 0x82, 0x53, 0x63) ++
    Bytes.concat(options.map(o => Bytes(o.optionType) ++ Bytes.fromLong(o.value.length,
    Bytes(0xff)
    }
    26

    View Slide

  27. test
    // udp payload
    val discover = "01010600a1290416000000000000000000000000000000000000000000ac79792a1a0000000000
    it("discover decode & encode") {
    // 本物のパケットのパース→バイト列への変換→パースを行っている
    val result = BootpPacket.parse(hexToBytes(discover))
    val value = result.get.value
    assert(value.bootpType === 1)
    assert(value.transactionId.toHexString === "a1290416")
    assert(value.yourClientIp.getHostAddress === "0.0.0.0")
    assert(value.clientMac.toString === "00:ac:79:79:2a:1a")
    assert(getBootpOption(value, BootpOptionType.DhcpMessageType).toInt() === DhcpMessageType
    assert(getBootpOption(value, BootpOptionType.RequestedIpAddress) === Bytes(0xc0, 0xa8,
    val packetBytes = value.asBytes
    assert(BootpPacket.parse(packetBytes).get.value === value)
    }
    27

    View Slide

  28. パース・パケット生成出来た
    28

    View Slide

  29. サーバを作る
    Java/ScalaでUDPなサーバを作る選択肢はいくつか有る
    java.net.DatagramSocket
    ブロードキャスト受信OK
    送信先にIPがまだ振られていないので、ARP解決出来なくて死亡
    pcap系
    送信に適してる
    http://jpcap.sourceforge.net/
    http://jnetpcap.com/
    送受信に適している
    https://www.pcap4j.org/ パケットのビルダー付属
    29

    View Slide

  30. pcap4j
    val nif: PcapNetworkInterface = Pcaps.getDevByName(deviceName) // uuid
    val nif: PcapNetworkInterface = Pcaps.getDevByAddress(address) // ip
    val handle: PcapHandle = nif.openLive(65536, PromiscuousMode.PROMISCUOUS, 10)
    handle.setFilter("udp and ( port 67 or port 68 )", BpfCompileMode.OPTIMIZE)
    handle.loop(packetCount = ‐1, new PacketListener {
    override def gotPacket(packet: Packet): Unit = handlePacket(packet)
    })
    def handlePacket(packet: Packet) = packet match {
    case eth: EthernetPacket =>
    println(s"Ethernet ${eth.getHeader.getSrcAddr} ‐> ${eth.getHeader.getDstAddr}")
    eth.getPayload match {
    case ipv4: IpV4packet =>
    ipv4.getPayload match {
    case udp: UdpPacket =>
    val arr = udp.getPayload.getRawData
    // process
    }
    }
    } 30

    View Slide

  31. pcap4j
    送信
    val src = InetAddress.getByName("10.0.0.1").asInstanceOf[Inet4Address]
    val dst = value.yourClientIp
    val newUdp = new UdpPacket.Builder()
    .srcAddr(src).dstAddr(dst)
    .srcPort(UdpPort.BOOTPS).dstPort(UdpPort.BOOTPC)
    .payloadBuilder(bootpPayload.getBuilder)
    .correctChecksumAtBuild(true)
    .correctLengthAtBuild(true)
    val newIp = new IpV4Packet.Builder(ipv4) // 受信したipv4パケットを継承
    .srcAddr(src).dstAddr(dst)
    .ttl(64)
    .payloadBuilder(newUdp)
    .correctChecksumAtBuild(true)
    .correctLengthAtBuild(true)
    val newEth = new EthernetPacket.Builder(eth)
    .srcAddr(macAddress).dstAddr(eth.getHeader.getSrcAddr)
    .payloadBuilder(newIp)
    .paddingAtBuild(true)
    pcapHandle.sendPacket(newEth.build()) // 送信
    31

    View Slide

  32. 受信に関しては、パケットに対してパターンマッチングが使えて便利
    ビルダーめんどう
    32

    View Slide

  33. DHCP
    サーバ
    紹介した手法を組み合わせて適当に固定アドレスを吐くサーバを定義
    33

    View Slide

  34. まとめ
    パーサーを書くのは楽しい
    拡張性・エラーメッセージの出力の容易さがメリット
    JSON等の複雑な記法も簡単にパースできる
    Scalaでネットワークプログラミングは出来る
    豊富なJavaライブラリ・Scalaの簡潔な記法
    一部のプロトコルのパースにはパーサコンビネータが適する
    pcap4j
    Java向けライブラリだが、一部Scalaのパターンマッチング等が使える
    パケットを簡単に組み立てるためのビルダーが付属する
    (C#でやった時に、チェックサムの計算に苦労した記憶が…)
    34

    View Slide

  35. おわり
    質疑応答
    35

    View Slide

  36. memo
    windowsでのDHCP再取得方法
    ipconfig /release
    ipconfig /renew
    36

    View Slide