ip65 technical reference

File : ip65/dns.s

 minimal dns implementation - requires a DNS server that supports recursion

functions

functiondescription
dns_resolve
 resolve a string containing a hostname (or a dotted quad) to an ip address
 inputs:
   cfg_dns must point to a DNS server that supports recursion
   dns_set_hostname must have been called to load the string to be resolved
 outputs: 
   carry flag is set if there was an error, clear otherwise 
   dns_ip: set to the  ip address of the hostname (if no error)
dns_set_hostname
 sets up for resolution of a hostname to an ip address
 inputs:
   AX = pointer to null terminated string that contains either a dns hostname
     (e.g. "host.example.com",0) or an address in "dotted quad" format,
     (e.g. "192.168.1.0",0)
 outputs: 
   carry flag is set on error (i.e. hostname too long), clear otherwise

variables

variabledescriptionsize (bytes)
dns_ipwill be contain ip address of hostname after succesful exection of dns_resolve 4
dns_status for debugging purposes only (behaviour not garuanteed) 2

implementation

; minimal dns implementation - requires a DNS server that supports recursion

  MAX_DNS_MESSAGES_SENT=8     ;timeout after sending 8 messages will be about 7 seconds (1+2+3+4+5+6+7+8)/4

.include "../inc/common.i"
.ifndef KPR_API_VERSION_NUMBER
  .define EQU     =
  .include "../inc/kipper_constants.i"
.endif

  .export dns_set_hostname
  .export dns_resolve
  .export dns_ip
  .export dns_status
  .import ip65_error
  .import cfg_dns
  
  .import parse_dotted_quad
  .import dotted_quad_value
  
  .import ip65_process
  
  .import udp_add_listener
  .import udp_remove_listener

  .import udp_callback
  .import udp_send

  .import udp_inp
  .import output_buffer
  .importzp udp_data

  .import udp_send_dest
  .import udp_send_src_port
  .import udp_send_dest_port
  .import udp_send_len
  .import check_for_abort_key
  .import timer_read
  
  .segment "IP65ZP" : zeropage

  dns_hostname: .res 2
  
  .bss

; dns packet offsets
dns_inp    = udp_inp + udp_data
dns_id  = 0
dns_flags=2
dns_qdcount=4
dns_ancount=6
dns_nscount=8
dns_arcount=10
dns_qname=12

dns_server_port=53
dns_client_port_low_byte: .res 1

dns_ip: .res 4  ;will be contain ip address of hostname after succesful exection of dns_resolve

dns_msg_id: .res 2

dns_current_label_length: .res 1
dns_current_label_offset: .res 1

dns_message_sent_count: .res 1

dns_packed_hostname: .res 128

; dns state machine
dns_initializing  = 1        ; initial state
dns_query_sent  = 2      ; sent a query, waiting for a response
dns_complete = 3        ; got a good response
dns_failed = 4        ; got either a 'no such name' or 'recursion declined' response

dns_state:  .res 1    ; flag indicating the current stage in the dns resolution process
dns_timer:  .res 1
dns_loop_count: .res 1
dns_break_polling_loop: .res 1

dns_status: .res 2  ; for debugging purposes only (behaviour not garuanteed)

hostname_copied:  .res 1

questions_in_response: .res 1

hostname_was_dotted_quad: .res 1

  .code
  
; sets up for resolution of a hostname to an ip address
; inputs:
;   AX = pointer to null terminated string that contains either a dns hostname
;     (e.g. "host.example.com",0) or an address in "dotted quad" format,
;     (e.g. "192.168.1.0",0)
; outputs: 
;   carry flag is set on error (i.e. hostname too long), clear otherwise
dns_set_hostname:  
  stax  dns_hostname
                                      ;copy the hostname into  a buffer suitable to copy directly into the qname field
                                      ;we need to split on dots

  jsr parse_dotted_quad      ; if we are passed an IP address instead of a hostname, don't bother looking it up in dns
  bcs @wasnt_dotted_quad
                             ;if the string was a dotted quad, then copy the parsed 4 bytes in to dns_ip
  lda #1
  sta hostname_was_dotted_quad
  ldx #3        ; set destination address
: lda dotted_quad_value,x
  sta dns_ip,x
  dex
  bpl :-
  
  rts     ;done!
  
@wasnt_dotted_quad:
  
  
  ldy #0                            ;input pointer
  ldx #1                            ;output pointer (start at 1, to skip first length offset, which will be filled in later)
  
  sty hostname_was_dotted_quad
  sty dns_current_label_length
  sty dns_current_label_offset
  sty hostname_copied
  
@next_hostname_byte:  
  lda (dns_hostname),y          ;get next char in hostname
  beq @end_of_hostname
  cmp #'/'                           ; allow hostnames to be terminated by "/" or ":" to help with URL parsing
  beq @end_of_hostname
  cmp #':'
  bne :+
@end_of_hostname:  
  inc hostname_copied
  bne @set_length_of_last_label
:

  cmp #'.'                         ;do we need to split the labels?
  bne @not_a_dot
@set_length_of_last_label:  
  txa
  pha 
  lda dns_current_label_length  
  ldx dns_current_label_offset
  sta  dns_packed_hostname,x
  lda #0
  sta dns_current_label_length
  pla
  tax
  stx dns_current_label_offset
  lda hostname_copied
  beq @update_counters 
  jmp @hostname_done
@not_a_dot:
  sta dns_packed_hostname,x
  inc dns_current_label_length
  
@update_counters:  
  iny  
  inx
  bmi @hostname_too_long    ;don't allow a hostname of more than 128 bytes
  jmp @next_hostname_byte

@hostname_done:
  
  lda dns_packed_hostname-1,x    ;get the last byte we wrote out
  beq :+                            ;was it a zero?
  lda #0
  sta dns_packed_hostname,x      ;write a trailing zero (i.e. a zero length label)  
  inx
:    
  clc   ;no error
  
  rts

@hostname_too_long:
  lda #KPR_ERROR_INPUT_TOO_LARGE
  sta ip65_error
  sec
  rts

; resolve a string containing a hostname (or a dotted quad) to an ip address
; inputs:
;   cfg_dns must point to a DNS server that supports recursion
;   dns_set_hostname must have been called to load the string to be resolved
; outputs: 
;   carry flag is set if there was an error, clear otherwise 
;   dns_ip: set to the  ip address of the hostname (if no error)
dns_resolve:  
  lda hostname_was_dotted_quad
  beq @hostname_not_dotted_quad
  clc
  rts     ;we already set dns_ip when copying the hostname
@hostname_not_dotted_quad:
  ldax #dns_in
  stax udp_callback 
  lda #53
  inc dns_client_port_low_byte    ;each call to resolve uses a different client address
  ldx dns_client_port_low_byte    ;so we don't get confused by late replies to a previous call
  jsr udp_add_listener
  
  bcc :+
  rts
:
  
  lda #dns_initializing
  sta dns_state
  lda #0  ;reset the "message sent" counter
  sta dns_message_sent_count
  
  jsr send_dns_query
  
@dns_polling_loop:
  lda dns_message_sent_count
  adc #1
  sta dns_loop_count       ;we wait a bit longer between each resend  
@outer_delay_loop: 
  lda #0
  sta dns_break_polling_loop
  jsr timer_read
  stx dns_timer            ;we only care about the high byte  
  
@inner_delay_loop:  
  jsr ip65_process
  jsr check_for_abort_key
  bcc @no_abort
  lda #KPR_ERROR_ABORTED_BY_USER
  sta ip65_error
  rts
@no_abort:  
  
  lda dns_state
  cmp #dns_complete
  beq @complete
  cmp #dns_failed
  beq @failed
   
  lda #0
  cmp dns_break_polling_loop
  bne @break_polling_loop
  jsr timer_read
  cpx dns_timer            ;this will tick over after about 1/4 of a second
  beq @inner_delay_loop
  
  dec dns_loop_count
  bne @outer_delay_loop  

@break_polling_loop:
  jsr send_dns_query  
  inc dns_message_sent_count
  lda dns_message_sent_count
  cmp #MAX_DNS_MESSAGES_SENT-1
  bpl @too_many_messages_sent
  jmp @dns_polling_loop
  
@complete:

  lda #53
  ldx dns_client_port_low_byte    
  jsr udp_remove_listener  
  rts

@too_many_messages_sent:
@failed:
  lda #53
  ldx dns_client_port_low_byte    
  jsr udp_remove_listener
  lda #KPR_ERROR_TIMEOUT_ON_RECEIVE
  sta ip65_error  
  sec             ;signal an error
  rts

send_dns_query:  
  ldax  dns_msg_id
  inx
  adc #0
  stax  dns_msg_id
  stax  output_buffer+dns_id

  ldax #$0001          ;QR =0 (query), opcode=0 (query), AA=0, TC=0,RD=1,RA=0,Z=0,RCODE=0
  stax output_buffer+dns_flags
  ldax #$0100          ;we ask 1 question 
  stax output_buffer+dns_qdcount
  ldax #$0000                 
  stax output_buffer+dns_ancount     ;we send no answers
  stax output_buffer+dns_nscount     ;we send no name servers
  stax output_buffer+dns_arcount     ;we send no authorative records
  
  ldx #0
:  
  lda dns_packed_hostname,x
  sta output_buffer+dns_qname,x
  inx
  bpl  @hostname_still_ok
  lda #KPR_ERROR_INPUT_TOO_LARGE
  sta ip65_error
  jmp @error_on_send                ;if we got past 128 bytes, there's a problem
@hostname_still_ok:  
  cmp #0
  bne :-                                  ;keep looping until we have a zero byte.
  
  lda #0
  sta output_buffer+dns_qname,x        ;high byte of QTYPE=1 (A)
  sta output_buffer+dns_qname+2,x     ;high byte of QLASS=1 (IN)
  lda #1
  sta output_buffer+dns_qname+1,x     ;low byte of QTYPE=1 (A)
  sta output_buffer+dns_qname+3,x     ;low byte of QLASS=1 (IN)
  
  txa
  clc
  adc #(dns_qname+4)
  ldx #0
  stax udp_send_len
  
  lda #53
  ldx dns_client_port_low_byte    
  stax udp_send_src_port

  ldx #3        ; set destination address
: lda cfg_dns,x
  sta udp_send_dest,x
  dex
  bpl :-

  ldax #dns_server_port      ; set destination port
  stax udp_send_dest_port
  ldax #output_buffer
  jsr udp_send  
  bcs @error_on_send
  lda #dns_query_sent
  sta dns_state

  rts
@error_on_send:  
  sec
  rts
  
dns_in:
  lda dns_inp+dns_flags+1 ;
  and #$0f   ;get the RCODE
  cmp #0    
  beq @not_an_error_response
  
  sta dns_status      ;anything non-zero is a permanent error (invalid domain, server doesn't support recursion etc)
  sta dns_status+1
  lda #dns_failed
  sta dns_state
  rts
@not_an_error_response:
  lda dns_inp+dns_qdcount+1
  sta questions_in_response
  cmp #1                          ;should be exactly 1 Q in the response (i.e. the one we sent)  
  beq :+
  jmp @error_in_response
:  
  lda dns_inp+dns_ancount+1
  bne :+
  jmp @error_in_response    ;should be at least 1 answer in response  
:                                      ;we need to skip over the question (we will assume it's the question we just asked)
  ldx #dns_qname              
:  
  lda dns_inp,x     ;get next length byte in question
  beq :+                            ; we're done if length==0
  clc
  txa
  
  adc dns_inp,x ;add length of next label to ptr
  adc #1                          ;+1 for the length byte itself
  tax
  bcs @error_in_response  ;if we overflowed x, then message is too big
  bcc :-
: 
  inx                               ;skip past the nul byte
  lda dns_inp+1,x
  cmp #1                          ;QTYPE should 1
  lda dns_inp+3,x
  cmp #1                          ;QCLASS should 1
  bne @error_in_response  
          
  inx                                 ;skip past the QTYPE/QCLASS
  inx
  inx
  inx

                                      ;x now points to the start of the answers
                                      
  lda  dns_inp,x
  bpl @error_in_response    ;we are expecting the high bit to be set (we assume the server will send us back the answer to the question we just asked)
  inx                               ;skip past the compression
  inx
                                    ;we are now pointing at the TYPE field
  lda  dns_inp+1,x            ;
    
  cmp #5                        ; is this a CNAME?
  bne @not_a_cname
  
  
  txa
  clc
  adc #10                       ;skip 2 bytes TYPE, 2 bytes CLASS, 4 bytes TTL, 2 bytes RDLENGTH
  tax
                                    ;we're now pointing at the CNAME record
  ldy #0                         ;start of CNAME hostname
:
  lda dns_inp,x
  beq @last_byte_of_cname
  bmi @found_compression_marker  
  sta dns_packed_hostname,y
  inx
  iny
  bmi @error_in_response  ;if we go past 128 bytes, something is wrong
  bpl :-                          ;go get next byte
  @last_byte_of_cname:
  sta dns_packed_hostname,y
  
  lda #$ff                       ;set a status marker so we know whats going on
  sta dns_status
  stx dns_status+1
  
  lda #1
  sta dns_break_polling_loop
  
  rts                              ; finished processing - the main dns polling loop should now resend a query, this time for the hostname from the CNAME record
  
@found_compression_marker:
  lda dns_inp+1,x
  tax
  jmp :-
  
@not_a_cname:  
  cmp #1                        ; should be 1 (A record)
  bne @error_in_response
  txa
  clc
  adc #10                       ;skip 2 bytes TYPE, 2 bytes CLASS, 4 bytes TTL, 2 bytes RDLENGTH
  tax
                                    ;we're now pointing at the answer!
  lda  dns_inp,x
  sta dns_ip

  lda  dns_inp+1,x
  sta dns_ip+1
  
  lda  dns_inp+2,x
  sta dns_ip+2
  
  lda  dns_inp+3,x
  sta dns_ip+3


  lda #dns_complete
  sta dns_state
  
  lda #1
  sta dns_break_polling_loop

@error_in_response:
  
  sta dns_status
  stx dns_status+1
  rts





;-- LICENSE FOR dns.s --
; The contents of this file are subject to the Mozilla Public License
; Version 1.1 (the "License"); you may not use this file except in
; compliance with the License. You may obtain a copy of the License at
; http://www.mozilla.org/MPL/
; 
; Software distributed under the License is distributed on an "AS IS"
; basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
; License for the specific language governing rights and limitations
; under the License.
; 
; The Original Code is ip65.
; 
; The Initial Developer of the Original Code is Jonno Downes,
; jonno@jamtronix.com.
; Portions created by the Initial Developer are Copyright (C) 2009
; Jonno Downes. All Rights Reserved.  
; -- LICENSE END --