Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

簡単な例 .! は変数抽出 !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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

wireshark見るとわかりやすい 19

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

DHCP Discover 21

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

型定義 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

Slide 24

Slide 24 text

ついでに列挙体も定義 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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

パケット生成 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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

おわり 質疑応答 35

Slide 36

Slide 36 text

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