dhcp/dhcp/messages.scm

318 lines
11 KiB
Scheme

;;; GNU Guix DHCP Client.
;;;
;;; Copyright © 2015 Rohan Prinja <rohan.prinja@gmail.com>
;;;
;;; This program is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; This program is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with this program. If not, see <http://www.gnu.org/licenses/>.
; Module for constructing and parsing DHCP messages
(define-module (dhcp messages)
#:export (<dhcp-message>
set-broadcast-bit
unset-broadcast-bit
option-value
serialize-dhcp-message
deserialize-dhcp-message
message-type
make-dhcpdiscover
map-type-to-code))
(use-modules (dhcp interfaces)
(dhcp dhcp)
(dhcp options base)
(dhcp options names)
(oop goops)
(rnrs base)
(rnrs bytevectors)
(rnrs enums))
; Magic cookie that starts off the 'options' field
; in a DHCP message packet.
(define *magic-cookie* #vu8(99 130 83 99))
; Valid types for a DHCP message.
(define *dhcp-message-types*
(make-enumeration '(DHCPDISCOVER
DHCPOFFER
DHCPREQUEST
DHCPDECLINE
DHCPACK
DHCPNAK
DHCPRELEASE
DHCPINFORM)))
; DHCP message object.
; See page 8, RFC 2131 for the message format.
(define-class <dhcp-message> ()
(op #:init-keyword #:op)
(htype #:init-keyword #:htype)
(hlen #:init-keyword #:hlen)
(hops #:init-form 0)
(xid #:init-keyword #:xid)
(secs #:init-keyword #:secs)
(flags #:init-form 0)
(ciaddr #:init-keyword #:ciaddr)
(yiaddr #:init-form (make-bytevector 4 0))
(siaddr #:init-form (make-bytevector 4 0))
(giaddr #:init-form (make-bytevector 4 0))
(chaddr #:init-keyword #:chaddr)
(sname #:init-form (make-bytevector 64 0))
(file #:init-form (make-bytevector 128 0))
; Options are represented as a fixed-length
; vector in which each element is either a
; <dhcp-option> object or #nil.
(options #:init-form (make-vector 256 #nil)
#:init-keyword #:options))
; Note: client initializes #hops to 0.
; Note: yiaddr, siaddr, giaddr are always 0 for
; client->server DHCP messages. See Page 32, RFC 2131.
; Set/unset the BROADCAST bit in the 'flags' field. The
; remaining bits are always zero, see Figure 2, RFC 2131.
(define-method (set-broadcast-bit (msg <dhcp-message>))
(slot-set! msg 'flags #x8000))
(define-method (unset-broadcast-bit (msg <dhcp-message>))
(slot-set! msg 'flags 0))
(define (serialize-options! opts dst idx)
"Copy the options field OPTS from a <dhcp-message> into a
bytevector. OPTS is a vector, DST is a bytevector.
Copying starts at index IDX in DST. This function mutates DST.
If an option is #nil, it means it does not exist, so it is
simply ignored whilst serializing."
(let loop ((i 0))
(if (< i 256)
(let* ((opt (vector-ref opts i)))
(if (eq? #nil opt)
(loop (1+ i))
(let ((code i)
(len (slot-ref opt 'len))
(val (slot-ref opt 'val)))
(begin
(if (zero? len)
(bytevector-u8-set! dst idx code)
(begin
(bytevector-u8-set! dst idx code)
(bytevector-u8-set! dst (1+ idx) len)
(bytevector-copy! val 0 dst (+ idx 2) len)))
(loop (1+ i)))))))))
; Serialize a <dhcp-message> object into a bytevector.
(define-method (serialize-dhcp-message (msg <dhcp-message>))
(let* ((res (make-bytevector 576 0))
(chaddr (slot-ref msg 'chaddr))
(chaddr-len (bytevector-length chaddr))
(padded-chaddr (make-bytevector 16 0))
(_ (bytevector-copy! chaddr 0
padded-chaddr (- 16 chaddr-len)
chaddr-len)))
(bytevector-u8-set! res 0 (slot-ref msg 'op))
(bytevector-u8-set! res 1 (slot-ref msg 'htype))
(bytevector-u8-set! res 2 (slot-ref msg 'hlen))
(bytevector-u8-set! res 3 (slot-ref msg 'hops))
(bytevector-u32-set! res 4 (slot-ref msg 'xid) (endianness big))
(bytevector-u16-set! res 8 (slot-ref msg 'secs) (endianness big))
(bytevector-u16-set! res 10 (slot-ref msg 'flags) (endianness big))
(bytevector-copy! (slot-ref msg 'ciaddr) 0 res 12 4)
(bytevector-copy! (slot-ref msg 'yiaddr) 0 res 16 4)
(bytevector-copy! (slot-ref msg 'siaddr) 0 res 20 4)
(bytevector-copy! (slot-ref msg 'giaddr) 0 res 24 4)
(bytevector-copy! padded-chaddr 0 res 28 16)
(bytevector-copy! (slot-ref msg 'sname) 0 res 44 64)
(bytevector-copy! (slot-ref msg 'file) 0 res 108 128)
(bytevector-copy! *magic-cookie* 0 res 236 4)
(serialize-options! (slot-ref msg 'options) res 240)
res))
; Read options from a bytevector 'src' starting at index
; 'idx' and returns a vector of <dhcp-option> objects.
(define (deserialize-options src idx)
(define (helper src i res)
(if (= i (bytevector-length src))
res ; nothing more to read from 'src'
(let* ((code (bytevector-u8-ref src i)))
(if (or (= code 0) (code 255))
(begin
(slot-set! res code (make-dhcp-option code 0 #nil))
(helper src (+ i 1) res))
(let* ((len (bytevector-u8-ref src (+ i 1)))
(val (make-bytevector len))
(_ (bytevector-copy! src (+ i 2) val 0 len)))
(begin
(slot-set! res code (make-dhcp-option code len val))
(helper src (+ i 2 len) res)))))))
(helper src idx (make-vector 256 #nil)))
; 'Pad' and 'End' are the only zero-length options.
; In RFC 4039, 'Rapid Commit' (also zero-length) was introduced.
; This is not yet supported in this client implementation.
;; (define (deserialize-dhcp-message msg)
;; "Given a serialized DHCP packet MSG, parse it and
;; return a <dhcp-message> object."
;; (let ((res (make <dhcp-message>)))
;; (slot-set! res 'op (bytevector-u8-ref msg 0))
;; (slot-set! res 'htype (bytevector-u8-ref msg 1))
;; (slot-set! res 'hlen (bytevector-u8-ref msg 2))
;; (slot-set! res 'hops (bytevector-u8-ref msg 3))
;; (slot-set! res 'xid (bytevector-u32-ref msg 4 (endianness big)))
;; (slot-set! res 'secs (bytevector-u16-ref msg 8 (endianness big)))
;; (slot-set! res 'flags (bytevector-u16-ref msg 10 (endianness big)))
;; (slot-set! res 'ciaddr (bytevector-u32-ref msg 12 (endianness big)))
;; (slot-set! res 'yiaddr (bytevector-u32-ref msg 16 (endianness big)))
;; (slot-set! res 'siaddr (bytevector-u32-ref msg 20 (endianness big)))
;; (slot-set! res 'giaddr (bytevector-u32-ref msg 24 (endianness big)))
;; (slot-set! res 'chaddr (bytevector-copy! msg 28 (slot-ref res 'chaddr) 0 16))
;; (slot-set! res 'sname (bytevector-copy! msg 44 (slot-ref res 'sname) 0 64))
;; (slot-set! res 'file (bytevector-copy! msg 108 (slot-ref res 'file) 0 128))
;; ; we skip the 4-byte magic cookie that starts off the options field
;; (slot-set! res 'options (deserialize-options msg 240))
;; res))
(define (bytevector-slice bv start len)
"Return a new bytevector with LEN elements sliced
from BV starting at index START"
(let ((res (make-bytevector len)))
(bytevector-copy! bv start res 0 len)
res))
(define (deserialize-dhcp-message msg)
(make <dhcp-message>
#:op (bytevector-u8-ref msg 0)
#:htype (bytevector-u8-ref msg 1)
#:hops (bytevector-u8-ref msg 2)
#:xid (bytevector-u32-ref msg 4 (endianness big))
#:secs (bytevector-u16-ref msg 8 (endianness big))
#:flags (bytevector-u16-ref msg 10 (endianness big))
#:ciaddr (bytevector-u32-ref msg 12 (endianness big))
#:yiaddr (bytevector-u32-ref msg 16 (endianness big))
#:siaddr (bytevector-u32-ref msg 20 (endianness big))
#:giaddr (bytevector-u32-ref msg 24 (endianness big))
; TODO: chaddr
#:options (deserialize-options msg 240)))
(define-method (set-option! (msg <dhcp-message>) (opt <dhcp-option>))
"Set an <option> in a <dhcp-message>."
(vector-set! (slot-ref msg 'options)
(slot-ref opt 'code)
opt))
(define-method (option-value (msg <dhcp-message>) code)
"Retrieve an option's value from a <dhcp-message>."
(let* ((opts (slot-ref msg 'options))
(opt (vector-ref opts code))
(val (slot-ref opt 'val)))
val))
; Get the DHCP message type. See Section 9.6, RFC 2132.
(define-syntax-rule (message-type msg)
(option-value msg 53))
; Map a DHCP message type to its single-digit code.
; See Section 9.6, RFC 2132.
(define-syntax-rule (map-type-to-code type)
(begin
(assert (enum-set-member? type *dhcp-message-types*))
(1+ ((enum-set-indexer *dhcp-message-types*) type))))
; Map a DHCP message type TYPE to its op.
; 1 = BOOTREQUEST, 2 = BOOTREPLY. See Pages 9, 27, 36 of
; RFC 2131.
(define-syntax-rule (map-type-to-op type)
(begin
(assert (enum-set-member? type *dhcp-message-types*))
(cond ((eq? 'DHCPOFFER type) 2)
((eq? 'DHCPACK type) 2)
((eq? 'DHCPNAK type) 2)
(else 1))))
(define (make-dhcp-message netif type opts)
"Make an instance of <dhcp-message> for interface NETIF
with message type TYPE and options initialized to OPTS"
(let* ((pair (slot-ref netif 'hwaddr))
(chaddr (car pair))
(htype (cdr pair))
(hlen (bytevector-length chaddr))
(op (map-type-to-op type))
(dhcp (slot-ref netif 'dhcp))
(msg-type-code (map-name-to-code
'DHCP-MESSAGE-TYPE)))
(begin
(vector-set! opts
msg-type-code ; 53
(make <dhcp-option>
#:code msg-type-code
#:len 1
#:val (make-bytevector 1 (map-type-to-code type))))
(make <dhcp-message>
#:op op
#:xid (retrieve-xid netif)
#:htype htype
#:hlen hlen
#:secs (retrieve-secs netif type)
#:chaddr chaddr
#:ciaddr (retrieve-ciaddr netif type)
#:options opts))))
(define (retrieve-xid netif)
"Given a <net-interface> NETIF, return the
its current transaction ID, unless it has just
started out, in which give it a new transaction
ID and return that"
(let* ((dhcp (slot-ref netif 'dhcp))
(state (slot-ref dhcp 'state)))
(if (eq? state 'DHCP-INIT)
(let* ((new-xid (generate-random-xid))
(_ (slot-set! dhcp 'xid new-xid)))
new-xid)
(slot-ref dhcp 'xid))))
(define (retrieve-ciaddr netif type)
"Given a <net-interface> NETIF and the message
type TYPE, return the appropriate value for the
ciaddr field in a <dhcp-message> object."
(let* ((dhcp (slot-ref netif 'dhcp))
(state (slot-ref dhcp 'state))
(zeroaddr (make-bytevector 4 0))
(ipaddr (slot-ref netif 'ipaddr)))
(cond ((or (eq? type 'DHCPDISCOVER)
(eq? type 'DHCPDECLINE))
zeroaddr)
((or (eq? type 'DHCPINFORM)
(eq? type 'DHCPRELEASE))
ipaddr)
((eq? type 'DHCPREQUEST)
(if (or (eq? state 'DHCP-BOUND)
(eq? state 'DHCP-RENEW)
(eq? state 'DHCP-REBIND))
ipaddr
zeroaddr)))))
; TODO: figure out from 2131 exactly when to
; return secs since config and when to return 0
(define (retrieve-secs netif type)
"Given a <net-interface> NETIF and the message
type TYPE, return the appropriate value for the
secs field in a <dhcp-message> object."
(let ((dhcp (slot-ref netif 'dhcp)))
(cond ((or (eq? type 'DHCPDECLINE)
(eq? type 'DHCPRELEASE))
0)
(else (- (current-time) ; might need to change
(slot-ref dhcp
'config-started-at))))))
(define-syntax-rule (make-dhcpdiscover netif opts)
(make-dhcp-message netif 'DHCPDISCOVER opts))