;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;                                                                 ;;
;; Copyright (C) KolibriOS team 2010-2015. All rights reserved.    ;;
;; Distributed under terms of the GNU General Public License       ;;
;;                                                                 ;;
;;  zeroconfig.asm - Zeroconfig service for KolibriOS              ;;
;;                                                                 ;;
;;  Written by hidnplayr@kolibrios.org                             ;;
;;    Some code contributed by Derpenguin                          ;;
;;                                                                 ;;
;;  DHCP code is based on that by Mike Hibbet                      ;;
;;      (DHCP client for menuetos)                                 ;;
;;                                                                 ;;
;;          GNU GENERAL PUBLIC LICENSE                             ;;
;;             Version 2, June 1991                                ;;
;;                                                                 ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

format binary as ""

; CONFIGURATION

TIMEOUT                 = 3             ; in seconds
BUFFER                  = 1024          ; in bytes
DHCP_TRIES              = 3             ; number of times to try contacting DHCP server
__DEBUG__               = 1             ; enable/disable
__DEBUG_LEVEL__         = 2             ; 1 = all, 2 = errors

; CONFIGURATION FOR LINK-LOCAL

PROBE_WAIT              = 1             ; second  (initial random delay)
PROBE_MIN               = 1             ; second  (minimum delay till repeated probe)
PROBE_MAX               = 2             ; seconds (maximum delay till repeated probe)
PROBE_NUM               = 3             ;         (number of probe packets)

ANNOUNCE_NUM            = 2             ;         (number of announcement packets)
ANNOUNCE_INTERVAL       = 2             ; seconds (time between announcement packets)
ANNOUNCE_WAIT           = 2             ; seconds (delay before announcing)

MAX_CONFLICTS           = 10            ;         (max conflicts before rate limiting)

RATE_LIMIT_INTERVAL     = 60            ; seconds (delay between successive attempts)

DEFEND_INTERVAL         = 10            ; seconds (min. wait between defensive ARPs)

MAX_INTERFACES          = 8

use32
        org     0x0

        db      'MENUET01'              ; 8 byte id
        dd      0x01                    ; header version
        dd      START                   ; start of code
        dd      IM_END                  ; size of image
        dd      (I_END+0x100)           ; memory for app
        dd      (I_END+0x100)           ; esp
        dd      0, 0                    ; I_Param, I_Path


include '../../proc32.inc'
include '../../macros.inc'
include '../../debug-fdo.inc'
include '../../network.inc'
include 'dhcp.inc'
include '../../dll.inc'

struct  dhcp_msg
        op              db ?    ; Operation Code
        htype           db ?    ; Hardware type
        hlen            db ?    ; Hardware address length
        hops            db ?
        xid             dd ?    ; Transaction Identifier
        secs            dw ?    ; Seconds since boot
        flags           dw ?
        ciaddr          dd ?    ; Client IP address
        yiaddr          dd ?    ; "Your" IP address
        siaddr          dd ?    ; Server IP address
        giaddr          dd ?    ; Gateway IP address
        chaddr          rb 16   ; Client hardware address
        sname           rb 64   ; Server name
        file            rb 128  ; boot filename
        cookie          dd ?    ; Magic cookie (0x63538263)
        options         rb 512
ends

struct  interface
        number          dd ?
        state           dd ?    ; 0 - disconnected, 1 - connected
        mode            dd ?    ; 0 - disabled, 1 - static, 2 - dhcp, 3 - auto (zero config)
        tries           dd ?
        lease           dd ?
        ServerIP        dd ?
        ip              dd ?
        subnet          dd ?
        dns             dd ?
        gateway         dd ?
        socketNum       dd ?
        timeout         dd ?
        ip_conflicts    dd ?
ends

START:
        mcall   68, 11                  ; init heap

        stdcall dll.Load, @IMPORT       ; load libraries
        or      eax, eax
        jnz     exit_immediately

        DEBUGF  2, "Zero-config service loaded\n"

        mcall   40, EVM_STACK2          ; We only want low-level network events

; Set up interface list
        mov     edi, device_list
        xor     ebx, ebx
  @@:
        inc     ebx
        mov     eax, ebx
        stosd
        mov     ecx, sizeof.interface/4-1
        xor     eax,eax
        rep stosd
        cmp     ebx, MAX_INTERFACES
        jb      @b

        mov     ebp, device_list
mainloop:
        cmp     [ebp + interface.state], 0
        je      .link_up?
        jmp     .maintain_link

  .next:
        cmp     [ebp + interface.number], MAX_INTERFACES
        je      .wait
        add     ebp, sizeof.interface
        jmp     mainloop

  .wait:
        mcall   10                      ; Wait for event
        mov     ebp, device_list
        jmp     mainloop

  .link_up?:
        mov     bh, byte[ebp + interface.number]
        mov     bl, 0                   ; Get device type
        mcall   74
        cmp     eax, 1                  ; Ethernet
        jne     mainloop.next

        mov     bl, 10                  ; Get Link status
        mcall   74
        test    eax, eax
        jz      mainloop.next

        mov     [ebp + interface.state], 1

        call    create_str_ini_int

; Try to read settings from .ini file
        invoke  ini.get_str, ini_path, str_ini_int, str_ip_type, inibuf, 16, str_null
        test    eax, eax
        jz      @f
; If settings not found, use default settings from 'ip?' section
        mov     dword[str_ini_int], 'ip?'
  @@:

        mov     ebx, API_ETH + 0
        mov     bh, byte[ebp + interface.number]
        mcall   76                      ; get MAC of the ethernet interface
        mov     word[tx_msg.chaddr], bx
        mov     dword[tx_msg.chaddr+2], eax
        DEBUGF  1, "MAC: %x-%x-%x-%x-%x-%x\n", \
        [tx_msg.chaddr+0]:2, [tx_msg.chaddr+1]:2, [tx_msg.chaddr+2]:2, \
        [tx_msg.chaddr+3]:2, [tx_msg.chaddr+4]:2, [tx_msg.chaddr+5]:2

        invoke  ini.get_str, ini_path, str_ini_int, str_ip_type, inibuf, 16, str_null
        test    eax, eax
        jnz     .invalid
        mov     eax, dword[inibuf]
        or      eax, 0x20202020
        mov     [ebp + interface.mode], 0
        cmp     eax, 'disa'
        je      .next
        mov     [ebp + interface.mode], 1
        cmp     eax, 'stat'
        je      static
        mov     [ebp + interface.mode], 2
        cmp     eax, 'dhcp'
        je      dhcp
        mov     [ebp + interface.mode], 3
        cmp     eax, 'auto'
        je      dhcp

  .invalid:
        DEBUGF  2, "Invalid settings for interface: %s.\n", str_ini_int
        jmp     .next

  .maintain_link:

; Check for IP conflicts
        mov     ebx, API_ARP
        mov     bh, byte[ebp + interface.number]
        mov     bl, 7
        mcall   76                      ; Number of IP conflicts
        cmp     eax, [ebp + interface.ip_conflicts]
        je      @f
        mov     [ebp + interface.ip_conflicts], eax
        DEBUGF  2, "IP address conflict on interface %u\n", [ebp + interface.number]
        ; Notify user of the IP address conflict
        mov     [notify_struct.msg], str_conflict
        mcall   70, notify_struct
  @@:

; Check if device is still there
        mov     bh, byte[ebp + interface.number]
        mov     bl, 0                   ; Get device type
        mcall   74
        test    eax, eax                ; No device
        jz      .link_down

; Check if link is still there
        mov     bl, 10                  ; Get Link status
        mcall   74
        test    eax, eax
        jnz     .next

  .link_down:
        mov     [ebp + interface.state], 0

; Notify user that the link is down
        mov     [notify_struct.msg], str_disconnected
        mcall   70, notify_struct

; CHECKME: should we do this in kernel instead? Should we even do this at all?
        xor     ecx, ecx
        mov     ebx, API_IPv4 + 3
        mov     bh, byte[ebp + interface.number]
        mcall   76                      ; ip
        mov     bl, 5
        mcall   76                      ; dns
        mov     bl, 7
        mcall   76                      ; subnet
        mov     bl, 9
        mcall   76                      ; gateway

        jmp     .next

link_up:

; Read number of previous IP conflicts
        mov     ebx, API_ARP
        mov     bh, byte[ebp + interface.number]
        mov     bl, 7
        mcall   76
        mov     [ebp + interface.ip_conflicts], eax

; Notify user that the link is up and running
        mov     [notify_struct.msg], str_connected
        mcall   70, notify_struct

  .fail:
        mcall   40, EVM_STACK2
        jmp     mainloop.next

static:
        DEBUGF  1, "Applying Static IP settings\n"

        invoke  ini.get_str, ini_path, str_ini_int, str_ip, inibuf, 16, str_null
        mov     esi, inibuf
        call    ip_str_to_dword
        mov     ebx, API_IPv4 + 3       ; set IP
        mov     bh, byte[ebp + interface.number]
        mcall   76

        invoke  ini.get_str, ini_path, str_ini_int, str_subnet, inibuf, 16, str_null
        mov     esi, inibuf
        call    ip_str_to_dword
        mov     ebx, API_IPv4 + 7       ; set subnet
        mov     bh, byte[ebp + interface.number]
        mcall   76

        invoke  ini.get_str, ini_path, str_ini_int, str_gateway, inibuf, 16, str_null
        mov     esi, inibuf
        call    ip_str_to_dword
        mov     ebx, API_IPv4 + 9       ; set gateway
        mov     bh, byte[ebp + interface.number]
        mcall   76

  .dns:
        invoke  ini.get_str, ini_path, str_ini_int, str_dns, inibuf, 16, str_null
        mov     esi, inibuf
        call    ip_str_to_dword
        mov     ebx, API_IPv4 + 5       ; set DNS
        mov     bh, byte[ebp + interface.number]
        mcall   76

        jmp     link_up


dhcp:

        DEBUGF  2, "Trying to contact DHCP server\n"

        mcall   40, EVM_STACK

        mcall   75, 0, AF_INET4, SOCK_DGRAM, 0                          ; open socket (parameters: domain, type, reserved)
        cmp     eax, -1
        je      dhcp_error
        mov     [ebp + interface.socketNum], eax

        DEBUGF  1, "Socket %x opened\n", eax

        mcall   75, 2, [ebp + interface.socketNum], sock_local, 18      ; bind socket to local port 68
        cmp     eax, -1
        je      socket_error

        DEBUGF  1, "Socket Bound to local port 68\n"

        pushd   [ebp + interface.number]
        pushd   4                       ; length of option
        pushd   1 shl 9                 ; SO_BINDTODEVICE
        pushd   0                       ; SOL_SOCKET
        mcall   75, 8, [ebp + interface.socketNum], esp
        add     esp, 16
        cmp     eax, -1
        je      socket_error

        DEBUGF  1, "Socket Bound to local interface %u\n", [ebp + interface.number]

        mcall   75, 4, [ebp + interface.socketNum], sock_remote, 18     ; connect to 255.255.255.255 on port 67
        cmp     eax, -1
        je      socket_error

        DEBUGF  1, "Connected to 255.255.255.255 on port 67\n"

        ; Read preferred IP address from settings file
        invoke  ini.get_str, ini_path, str_ini_int, str_ip, inibuf, 16, str_null
        mov     esi, inibuf
        call    ip_str_to_dword
        mov     [ebp + interface.ip], ecx

        call    random
        mov     [tx_msg.xid], eax                                       ; randomize session ID
        mov     [tx_msg_type], 1                                        ; DHCP discover

build_dhcp_packet:

        DEBUGF  1, "Building DHCP packet\n"

        mov     [ebp + interface.tries], DHCP_TRIES

        ; Boot protocol legacy
        mov     [tx_msg.op], 1                                          ; Boot request
        mov     [tx_msg.htype], 1                                       ; Ethernet
        mov     [tx_msg.hlen], 6                                        ; Ethernet address h/w len
        mov     [tx_msg.hops], 0
        mcall   26, 9                                                   ; Time since boot
        xor     edx, edx
        mov     ebx, 100
        div     ebx                                                     ; Divide by 100 to get number of seconds
        mov     [tx_msg.secs], ax
        mov     [tx_msg.flags], 0

        ; DHCP extension
        mov     [tx_msg.cookie], 0x63538263                             ; magic cookie

        mov     word[tx_msg+240], 0x0135                                ; option DHCP msg type
        mov     al,[tx_msg_type]
        mov     [tx_msg+240+2], al

        mov     word[tx_msg+240+3], 0x0433                              ; option Lease time
        mov     dword[tx_msg+240+5], -1                                 ; infinite

        mov     word[tx_msg+240+9], 0x0432                              ; option requested IP address
        mov     eax,[ebp + interface.ip]
        mov     [tx_msg+240+11], eax

        mov     word[tx_msg+240+15], 0x0437                             ; option request list
        mov     dword[tx_msg+240+17], 0x0f060301

        cmp     [tx_msg_type], 1                                        ; Check which msg we are sending
        jne     .request

        mov     byte[tx_msg+240+21], 0xff                               ; end of options marker

        mov     [tx_msg_len], 262                                       ; length
        jmp     send_dhcp_packet

  .request:
        mov     word[tx_msg+240+21], 0x0436                             ; server IP
        mov     eax,[ebp + interface.ServerIP]
        mov     [tx_msg+240+23], eax

        mov     byte[tx_msg+240+27], 0xff                               ; end of options marker

        mov     [tx_msg_len], 268                                       ; length


send_dhcp_packet:
        DEBUGF  1, "Sending DHCP packet\n"
        lea     edx, [tx_msg]
        mcall   75, 6, [ebp + interface.socketNum], , [tx_msg_len]

; Wait for reply
        mcall   26, 9
        add     eax, TIMEOUT*100
        mov     [ebp + interface.timeout], eax
        mov     ebx, TIMEOUT*100
  .wait:
        mcall   23                                                      ; Wait for event with timeout
read_packet:                                                            ; we have data - this will be the response
        lea     edx, [rx_msg]
        mcall   75, 7, [ebp + interface.socketNum], , BUFFER, MSG_DONTWAIT    ; read data from socket
        cmp     eax, -1
        jne     .got_data

        mcall   26, 9
        mov     ebx, eax
        sub     ebx, [ebp + interface.timeout]
        ja      send_dhcp_packet.wait

        DEBUGF  2, "No answer from DHCP server\n"
        dec     [ebp + interface.tries]
        jnz     send_dhcp_packet
        jmp     dhcp_fail

  .got_data:
        DEBUGF  1, "%d bytes received\n", eax
        mov     [rx_msg_len], eax

; depending on which msg we sent, handle the response
; accordingly.
; If the response is to a dhcp discover, then:
;  1) If response is DHCP OFFER then
;  1.1) record server IP, lease time & IP address.
;  1.2) send a request packet
; If the response is to a dhcp request, then:
;  1) If the response is DHCP ACK then
;  1.1) extract the DNS & subnet fields. Set them in the stack

        cmp     [tx_msg_type], 1                ; did we send a discover?
        je      discover_sent
        cmp     [tx_msg_type], 3                ; did we send a request?
        je      request_sent
        jmp     exit_immediately

discover_sent:
        call    parse_dhcp_reply
        cmp     [rx_msg_type], 2                ; Was the response an offer?
        jne     read_packet

        DEBUGF  1, "Got offer, making request\n"
        mov     [tx_msg_type], 3                ; make it a request
        jmp     build_dhcp_packet

request_sent:
        call    parse_dhcp_reply
        cmp     [rx_msg_type], 5                ; Was the response an ACK? It should be
        jne     read_packet                     ; NO - read next packets

        DEBUGF  2, "IP assigned by DHCP server successfully\n"

        mcall   close, [ebp + interface.socketNum]

        mov     ebx, API_IPv4 + 3
        mov     bh, byte[ebp + interface.number]
        mcall   76, , [ebp + interface.ip]            ; ip
        mov     bl, 7
        mcall   76, , [ebp + interface.subnet]        ; subnet
        mov     bl, 9
        mcall   76, , [ebp + interface.gateway]       ; gateway

        invoke  ini.get_str, ini_path, str_ini_int, str_dns_type, inibuf, 16, str_null
        test    eax, eax
        jnz     @f
        mov     eax, dword[inibuf]
        or      eax, 0x202020
        cmp     eax, 'stat'
        je      static.dns
  @@:
        mov     ebx, API_IPv4 + 5
        mov     bh, byte[ebp + interface.number]
        mcall   76, , [ebp + interface.dns]           ; dns

        jmp     link_up


parse_dhcp_reply:

        DEBUGF  1, "Parsing response\n"
        mov     [rx_msg_type], 0

; Verify if session ID matches
        mov     eax, [tx_msg.xid]
        cmp     [rx_msg.xid], eax
        jne     .done

        pushd   [rx_msg.yiaddr]
        pop     [ebp + interface.ip]
        DEBUGF  1, "Client: %u.%u.%u.%u\n", \
        [rx_msg.yiaddr]:1, [rx_msg.yiaddr+1]:1, [rx_msg.yiaddr+2]:1, [rx_msg.yiaddr+3]:1

; Verify magic cookie
        cmp     [rx_msg.cookie], 0x63538263
        jne     .done

; Parse the DHCP options
        lea     esi, [rx_msg]
        mov     ecx, 240                        ; point to the first option
  .next_option:
; TODO: check if we still are inside the buffer!
        add     esi, ecx

        lodsb                                   ; get message identifier
        mov     bl, al
        cmp     bl, 0xff                        ; End of options?
        je      .done
        test    bl, bl
        jz      .pad

        lodsb                                   ; load data length
        movzx   ecx, al
        cmp     bl, dhcp_msg_type               ; Msg type is a single byte option
        je      .msgtype
        cmp     bl, dhcp_dhcp_server_id
        je      .server
        cmp     bl, dhcp_address_time
        je      .lease
        cmp     bl, dhcp_subnet_mask
        je      .subnet
        cmp     bl, dhcp_router
        je      .router
        cmp     bl, dhcp_domain_server
        je      .dns

        DEBUGF  1, "Unsupported DHCP option: %u\n", bl
        jmp     .next_option

  .pad:
        xor     ecx, ecx
        inc     ecx
        jmp     .next_option

  .msgtype:
        mov     al, [esi]
        mov     [rx_msg_type], al

        DEBUGF  1, "DHCP Msg type: %u\n", al
        jmp     .next_option                    ; Get next option

  .server:
        pushd   [esi]
        pop     [ebp + interface.ServerIP]
        DEBUGF  1, "Server: %u.%u.%u.%u\n", [esi]:1, [esi+1]:1, [esi+2]:1, [esi+3]:1
        jmp     .next_option

  .lease:
        pusha
        mov     eax,[esi]
        bswap   eax
        mov     [ebp + interface.lease], eax
        DEBUGF  1, "Lease: %d\n", eax
        popa
        jmp     .next_option

  .subnet:
        pushd   [esi]
        pop     [ebp + interface.subnet]
        DEBUGF  1, "Subnet: %u.%u.%u.%u\n", [esi]:1, [esi+1]:1, [esi+2]:1, [esi+3]:1
        jmp     .next_option

  .router:
        pushd   [esi]
        pop     [ebp + interface.gateway]
        DEBUGF  1, "Gateway: %u.%u.%u.%u\n", [esi]:1, [esi+1]:1, [esi+2]:1, [esi+3]:1
        jmp     .next_option

  .dns:
        pushd   [esi]
        pop     [ebp + interface.dns]
        DEBUGF  1, "DNS: %u.%u.%u.%u\n", [esi]:1, [esi+1]:1, [esi+2]:1, [esi+3]:1
        jmp     .next_option

  .done:
        ret

exit_immediately:
        DEBUGF  2, "Zeroconf failed!\n"
        mcall   -1

socket_error:
        DEBUGF  2, "Socket error!\n"

dhcp_fail:
        mcall   close, [ebp + interface.socketNum]

dhcp_error:
        DEBUGF  1, "DHCP failed\n"
        cmp     [ebp + interface.mode], 3               ; zero config mode?
        jne     link_up

link_local:

; TODO: send ARP probes before setting the IP address in stack!

        call    random
        mov     cx, ax
        shl     ecx, 16
        mov     cx, 0xfea9                              ; IP 169.254.0.0 link local net, see RFC3927
        mov     ebx, API_IPv4 + 3
        mov     bh, byte[ebp + interface.number]
        mcall   76, , ecx                               ; mask is 255.255.0.0
        DEBUGF  2, "Link Local IP assigned: 169.254.%u.%u\n", [generator+0]:1, [generator+1]:1
        mov     bl, 7
        mcall   76, , 0xffff
        mov     bl, 9
        mcall   76, , 0x0
        mov     bl, 5
        mcall   76, , 0x0

        jmp     link_up


random:  ; Pseudo random actually

        mov     eax,[generator]
        add     eax, -43ab45b5h
        ror     eax, 1
        bswap   eax
        xor     eax, dword[tx_msg.chaddr]
        ror     eax, 1
        xor     eax, dword[tx_msg.chaddr+2]
        mov     [generator], eax

        ret



create_str_ini_int:
        mov     eax, [ebp + interface.number]
        mov     ebx, 10
        xor     edx, edx
        push    0
  @@:
        div     ebx
        add     dl, '0'
        push    edx
        test    eax, eax
        jnz     @r
  @@:
        mov     edi, str_ini_int+2
  @@:
        pop     eax
        stosb
        test    eax, eax
        jnz     @r

        ret



; In: esi = ptr to ASCIIZ IP address
; Out: ecx = IP (0 on error)

ip_str_to_dword:

        xor     ecx, ecx        ; end result
  .charloop:
        lodsb
        test    al, al
        jz      .finish
        cmp     al, '.'
        je      .dot
        sub     al, '0'
        jb      .fail
        cmp     al, 9
        ja      .fail
        mov     dl, cl
        shl     cl, 2
        jc      .fail
        add     cl, dl
        jc      .fail
        add     cl, cl
        jc      .fail
        add     cl, al
        jc      .fail
        jmp     .charloop
  .dot:
        shl     ecx, 8
        jc      .fail
        xor     cl, cl
        jmp     .charloop
  .finish:
        bswap   ecx             ; we want little endian order
        ret

  .fail:
        xor     ecx, ecx
        ret

; DATA AREA

align 16
@IMPORT:

library \
        libini,         'libini.obj'

import  libini, \
        ini.get_str,    'ini_get_str',\
        ini.set_str,    'ini_set_str'

include_debug_strings

str_ip          db 'ip', 0
str_subnet      db 'subnet', 0
str_gateway     db 'gateway', 0
str_dns         db 'dns', 0

str_ip_type     db 'ip_type', 0
str_dns_type    db 'dns_type', 0

str_ini_int     db 'ip1', 0
                rb 10

str_null        db 0

sock_local:
        dw AF_INET4
        dw 68 shl 8     ; local port
        dd 0            ; local IP
        rb 10


sock_remote:
        dw AF_INET4
        dw 67 shl 8     ; destination port
        dd -1           ; destination IP
        rb 10

notify_struct:
        dd 7            ; run application
        dd 0
 .msg   dd 0
        dd 0
        dd 0
        db '/sys/@notify', 0

str_connected           db '"You are now connected to the network." -N', 0
str_disconnected        db '"You are now disconnected from the network." -N', 0
str_conflict            db '"An IP address conflict has been detected on the network." -W', 0

ini_path                db '/sys/settings/network.ini',0

IM_END:

generator       dd ?

inibuf          rb 16

tx_msg_len      dd ?
rx_msg_len      dd ?
tx_msg_type     db ?
rx_msg_type     db ?
tx_msg          dhcp_msg
rx_msg          dhcp_msg

device_list     rd MAX_INTERFACES*sizeof.interface

I_END: