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

4e842bca931f66f9cf29b3fdbd2cc4d8?s=47 kamijin_fanta
November 11, 2018
430

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

4e842bca931f66f9cf29b3fdbd2cc4d8?s=128

kamijin_fanta

November 11, 2018
Tweet

Transcript

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

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

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

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

  5. パーサコンビネータ プログラミング言語等の構文解析などを行うライブラリ xml, json, js, etc... 汎用プログラミング言語で行うことが特徴 DSLとかが記述しやすい・関数型言語での実装が多い js, scala,

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

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

  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
  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
  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
  11. val json = JsArray(List( JsObject(Map( "id" ‐> JsNumber(1234), "name" ‐>

    JsString("hoge") )) )) [{ "id": 1234, "name": "hoge"}] 11
  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
  13. パーサー書くの楽しい! 世はテキストに溢れている… HTTP・URL・DSL デメリットを忘れない(遅い) https://www.lihaoyi.com/fastparse/#Performance 13

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

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

  16. DHCP Dynamic Host Configuration Protocol(ダイナミック ホスト コンフィギュレーション プロトコル、DHCP)とは、コンピュータがネットワーク接続する際に必要な情報を自 動的に割り当てるプロトコルのことをいう。 DHCP

    は BOOTP の上位互換であり、メッセージ構造などは変わっていない。DHCP で は、BOOTP に比べて自動設定できる情報が増え、より使いやすくなっている。 https://ja.wikipedia.org/wiki/Dynamic̲Host̲Configuration̲Protocol 自動設定可能な項目 IPアドレス・デフォルトゲートウェイ・DNS ホスト名 NTP プリンター 16
  17. DHCP プロトコル DHCPサーバは、予めアドレスレンジを確保しておく (Discovery) ブロードキャストのUDPメッセージでDHCPサーバを探す (Offer) DHCPサーバは、アドレス・オプション値などを返す (Request) IPが競合していないことを確認してからアドレスの割当依頼を送信 (Ack)

    割当許可 https://ja.wikipedia.org/wiki/Dynamic̲Host̲Configuration̲Protocol 17
  18. DHCP パケット https://www.infraexpert.com/study/tcpip13.html UDP サーバ: Port67 クライアント: Port68 パケット構造は、上り下りで全く同じ (UDPポート・メッセージタイプ・オプション値

    が異なる) 18
  19. wireshark見るとわかりやすい 19

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

  21. DHCP Discover 21

  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
  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
  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
  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
  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
  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
  28. パース・パケット生成出来た 28

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

    https://www.pcap4j.org/ パケットのビルダー付属 29
  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
  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
  32. 受信に関しては、パケットに対してパターンマッチングが使えて便利 ビルダーめんどう 32

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

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

    (C#でやった時に、チェックサムの計算に苦労した記憶が…) 34
  35. おわり 質疑応答 35

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