#lang imperative-syndicate

(provide (struct-out ip-packet)
	 ip-address->hostname
	 ip-string->ip-address
	 apply-netmask
	 ip-address-in-subnet?
         query-local-ip-addresses
	 broadcast-ip-address
	 spawn-ip-driver)

(require racket/set)
(require (only-in racket/string string-split))
(require bitsyntax)

(require "dump-bytes.rkt")
(require "configuration.rkt")
(require "checksum.rkt")

(require/activate imperative-syndicate/drivers/timer)
(require/activate "ethernet.rkt")
(require/activate "arp.rkt")

(message-struct ip-packet
                (source-interface ;; string for an ethernet interface, or #f for local interfaces
                 source
                 destination
                 protocol
                 options
                 body
                 ;; TODO: more fields
                 ))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(define (ip-address->hostname bs)
  (bit-string-case bs
    ([ a b c d ] (format "~a.~a.~a.~a" a b c d))))

(define (ip-string->ip-address str)
  (list->bytes (map string->number (string-split str "."))))

(define (apply-netmask addr netmask)
  (bit-string-case addr
    ([ (n :: integer bytes 4) ]
     (bit-string ((bitwise-and n (arithmetic-shift #x-100000000 (- netmask)))
		  :: integer bytes 4)))))

(define (ip-address-in-subnet? addr network netmask)
  (equal? (apply-netmask network netmask)
	  (apply-netmask addr netmask)))

(define broadcast-ip-address (bytes 255 255 255 255))

(define (query-local-ip-addresses)
  (query-set local-ips (host-route $addr _ _) addr))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(define (spawn-ip-driver)
  (spawn #:name 'ip-driver
   (during/spawn (host-route $my-address $netmask $interface-name)
                 (assert (route-up (host-route my-address netmask interface-name)))
                 (do-host-route my-address netmask interface-name))
   (during/spawn (gateway-route $network $netmask $gateway-addr $interface-name)
                 (assert (route-up (gateway-route network netmask gateway-addr interface-name)))
                 (do-gateway-route network netmask gateway-addr interface-name))
   (during/spawn (net-route $network-addr $netmask $link)
                 (assert (route-up (net-route network-addr netmask link)))
                 (do-net-route network-addr netmask link))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Local IP route

(define (do-host-route my-address netmask interface-name)
  (let ((network-addr (apply-netmask my-address netmask)))
    (do-normal-ip-route (host-route my-address netmask interface-name)
                        network-addr
                        netmask
                        interface-name))

  (assert (arp-assertion IPv4-ethertype my-address interface-name))
  (on (message (ip-packet _ $peer-address my-address PROTOCOL-ICMP _ $body))
      (bit-string-case body
        ([ type code (checksum :: integer bytes 2) (rest :: binary) ] ;; TODO: check cksum
         (case type
           [(8) ;; ECHO (0 is ECHO-REPLY)
            (log-info "Ping of ~a from ~a"
                      (pretty-bytes my-address)
                      (pretty-bytes peer-address))
            (define reply-data0 (bit-string 0
                                            code
                                            (0 :: integer bytes 2) ;; TODO
                                            (rest :: binary)))
            (send! (ip-packet #f
                              my-address
                              peer-address
                              PROTOCOL-ICMP
                              #""
                              (ip-checksum 2 reply-data0)))]
           [else
            (log-info "ICMP ~a/~a (cksum ~a) to ~a from ~a:\n~a"
                      type
                      code
                      checksum
                      (pretty-bytes my-address)
                      (pretty-bytes peer-address)
                      (dump-bytes->string rest))]))
        (else #f))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Gateway IP route

(define (do-gateway-route network netmask gateway-addr interface-name)
  (define the-route (gateway-route network netmask gateway-addr interface-name))

  (field [routes (set)])
  (query-set* routes (host-route $addr $netmask _) (list addr netmask))
  (query-set* routes (gateway-route $addr $netmask _ _) (list addr netmask))
  (query-set* routes (net-route $addr $netmask _) (list addr netmask))

  (define (covered-by-some-other-route? addr)
    (for/or ([r (in-set (routes))])
      (match-define (list net msk) r)
      (and (positive? msk)
	   (ip-address-in-subnet? addr net msk))))

  (during (ethernet-interface interface-name $gateway-interface-hwaddr)
    (field [gateway-hwaddr #f])
    (on (asserted (arp-query IPv4-ethertype gateway-addr interface-name $hwaddr))
        (when (not (gateway-hwaddr))
          (log-info "Discovered gateway ~a at ~a on interface ~a."
                    (ip-address->hostname gateway-addr)
                    interface-name
                    (pretty-bytes hwaddr)))
        (gateway-hwaddr hwaddr))

    (on (message ($ p (ip-packet _ _ _ _ _ _)))
        (when (not (gateway-hwaddr))
          (log-warning "Gateway hwaddr for ~a not known, packet dropped."
                       (ip-address->hostname gateway-addr)))
        (when (and (gateway-hwaddr)
                   (not (equal? (ip-packet-source-interface p) interface-name))
                   (not (covered-by-some-other-route? (ip-packet-destination p))))
          (send! (ethernet-packet interface-name
                                  #f
                                  gateway-interface-hwaddr
                                  (gateway-hwaddr)
                                  IPv4-ethertype
                                  (format-ip-packet p)))))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; General net route

(define (do-net-route network-addr netmask link)
  (do-normal-ip-route (net-route network-addr netmask link) network-addr netmask link))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Normal IP route

(define (do-normal-ip-route the-route network netmask interface-name)
  (assert (arp-interface interface-name))
  (on (message (ethernet-packet interface-name #t _ _ IPv4-ethertype $body))
      (define p (parse-ip-packet interface-name body))
      (when p (send! p)))
  (during (ethernet-interface interface-name $interface-hwaddr)
    (on (message ($ p (ip-packet _ _ _ _ _ _)))
        (define destination (ip-packet-destination p))
        (when (and (not (equal? (ip-packet-source-interface p) interface-name))
                   (ip-address-in-subnet? destination network netmask))
          ;; v Use `spawn` instead of `react` to avoid gratuitous packet
          ;; reordering.
          (spawn (stop-when-timeout 5000
                   (log-warning "ARP lookup of ~a failed, packet dropped"
                                (ip-address->hostname destination)))
                 (stop-when (asserted (arp-query IPv4-ethertype
                                                 destination
                                                 interface-name
                                                 $destination-hwaddr))
                   (send! (ethernet-packet interface-name
                                           #f
                                           interface-hwaddr
                                           destination-hwaddr
                                           IPv4-ethertype
                                           (format-ip-packet p)))))))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(define IPv4-ethertype #x0800)

(define IP-VERSION 4)
(define IP-MINIMUM-HEADER-LENGTH 5)

(define PROTOCOL-ICMP 1)

(define default-ttl 64)

(define (parse-ip-packet interface-name body)
  ;; (log-info "IP ~a got body ~a" (pretty-bytes my-address) (pretty-bytes body))
  (bit-string-case body
    ([ (= IP-VERSION :: bits 4)
       (header-length :: bits 4)
       service-type
       (total-length :: bits 16)
       (id :: bits 16)
       (flags :: bits 3)
       (fragment-offset :: bits 13)
       ttl
       protocol
       (header-checksum :: bits 16) ;; TODO: check checksum
       (source-ip0 :: binary bits 32)
       (destination-ip0 :: binary bits 32)
       (rest :: binary) ]
     (let* ((source-ip (bit-string->bytes source-ip0))
	    (destination-ip (bit-string->bytes destination-ip0))
	    (options-length (* 4 (- header-length IP-MINIMUM-HEADER-LENGTH)))
	    (data-length (- total-length (* 4 header-length))))
       (if (and (>= header-length 5)
		(>= (bit-string-byte-count body) (* header-length 4)))
	   (bit-string-case rest
	     ([ (opts :: binary bytes options-length)
		(data :: binary bytes data-length)
		(:: binary) ] ;; Very short ethernet packets have a trailer of zeros
	      (ip-packet interface-name
			 (bit-string->bytes source-ip)
			 (bit-string->bytes destination-ip)
			 protocol
			 (bit-string->bytes opts)
			 (bit-string->bytes data))))
	   #f)))
    (else #f)))

(define (format-ip-packet p)
  (match-define (ip-packet _ src dst protocol options body) p)

  (define header-length ;; TODO: ensure options is a multiple of 4 bytes
    (+ IP-MINIMUM-HEADER-LENGTH (quotient (bit-string-byte-count options) 4)))

  (define header0 (bit-string (IP-VERSION :: bits 4)
			      (header-length :: bits 4)
			      0 ;; TODO: service type
			      ((+ (* header-length 4) (bit-string-byte-count body))
			       :: bits 16)
			      (0 :: bits 16)  ;; TODO: identifier
			      (0 :: bits 3)   ;; TODO: flags
			      (0 :: bits 13)  ;; TODO: fragments
			      default-ttl
			      protocol
			      (0 :: bits 16)
			      (src :: binary bits 32)
			      (dst :: binary bits 32)
			      (options :: binary)))
  (define full-packet (bit-string ((ip-checksum 10 header0) :: binary) (body :: binary)))

  full-packet)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(spawn-ip-driver)
