// DHCP look-up functions based on the udp client // http://www.ietf.org/rfc/rfc2131.txt // // Author: Andrew Lindsay // Rewritten and optimized by Jean-Claude Wippler, http://jeelabs.org/ // // Rewritten dhcpStateMachine by Chris van den Hooven // as to implement dhcp-renew when lease expires (jun 2012) // // Various modifications and bug fixes contributed by Victor Aprea (oct 2012) // // Copyright: GPL V2 // See http://www.gnu.org/licenses/gpl.html //#define DHCPDEBUG #include "EtherCard.h" #include "net.h" #define gPB ether.buffer #define DHCP_BOOTREQUEST 1 #define DHCP_BOOTRESPONSE 2 // DHCP Message Type (option 53) (ref RFC 2132) #define DHCP_DISCOVER 1 #define DHCP_OFFER 2 #define DHCP_REQUEST 3 #define DHCP_DECLINE 4 #define DHCP_ACK 5 #define DHCP_NAK 6 #define DHCP_RELEASE 7 // DHCP States for access in applications (ref RFC 2131) enum { DHCP_STATE_INIT, DHCP_STATE_SELECTING, DHCP_STATE_REQUESTING, DHCP_STATE_BOUND, DHCP_STATE_RENEWING, }; /* op 1 Message op code / message type. 1 = BOOTREQUEST, 2 = BOOTREPLY htype 1 Hardware address type, see ARP section in "Assigned Numbers" RFC; e.g., '1' = 10mb ethernet. hlen 1 Hardware address length (e.g. '6' for 10mb ethernet). hops 1 Client sets to zero, optionally used by relay agents when booting via a relay agent. xid 4 Transaction ID, a random number chosen by the client, used by the client and server to associate messages and responses between a client and a server. secs 2 Filled in by client, seconds elapsed since client began address acquisition or renewal process. flags 2 Flags (see figure 2). ciaddr 4 Client IP address; only filled in if client is in BOUND, RENEW or REBINDING state and can respond to ARP requests. yiaddr 4 'your' (client) IP address. siaddr 4 IP address of next server to use in bootstrap; returned in DHCPOFFER, DHCPACK by server. giaddr 4 Relay agent IP address, used in booting via a relay agent. chaddr 16 Client hardware address. sname 64 Optional server host name, null terminated string. file 128 Boot file name, null terminated string; "generic" name or null in DHCPDISCOVER, fully qualified directory-path name in DHCPOFFER. options var Optional parameters field. See the options documents for a list of defined options. */ // size 236 typedef struct { byte op, htype, hlen, hops; uint32_t xid; uint16_t secs, flags; byte ciaddr[IP_LEN], yiaddr[IP_LEN], siaddr[IP_LEN], giaddr[IP_LEN]; byte chaddr[16], sname[64], file[128]; // options } DHCPdata; #define DHCP_SERVER_PORT 67 #define DHCP_CLIENT_PORT 68 // timeouts im ms #define DHCP_REQUEST_TIMEOUT 10000 #define DHCP_HOSTNAME_MAX_LEN 32 // RFC 2132 Section 3.3: // The time value of 0xffffffff is reserved to represent "infinity". #define DHCP_INFINITE_LEASE 0xffffffff static byte dhcpState = DHCP_STATE_INIT; static char hostname[DHCP_HOSTNAME_MAX_LEN] = "Arduino-ENC28j60-00"; // Last two characters will be filled by last 2 MAC digits ; static uint32_t currentXid; static uint32_t stateTimer; static uint32_t leaseStart; static uint32_t leaseTime; static byte* bufPtr; static uint8_t dhcpCustomOptionNum = 0; static DhcpOptionCallback dhcpCustomOptionCallback = NULL; extern uint8_t allOnes[];// = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; static void addToBuf (byte b) { *bufPtr++ = b; } static void addBytes (byte len, const byte* data) { while (len-- > 0) addToBuf(*data++); } static void addOption (byte opt, byte len, const byte* data) { addToBuf(opt); addToBuf(len); addBytes(len, data); } // Main DHCP sending function // implemented // state / msgtype // INIT / DHCPDISCOVER // SELECTING / DHCPREQUEST // BOUND (RENEWING) / DHCPREQUEST // ---------------------------------------------------------- // | |SELECTING |RENEWING |INIT | // ---------------------------------------------------------- // |broad/unicast |broadcast |unicast |broadcast | // |server-ip |MUST |MUST NOT |MUST NOT | option 54 // |requested-ip |MUST |MUST NOT |MUST NOT | option 50 // |ciaddr |zero |IP address |zero | // ---------------------------------------------------------- // options used (both send/receive) #define DHCP_OPT_SUBNET_MASK 1 #define DHCP_OPT_ROUTERS 3 #define DHCP_OPT_DOMAIN_NAME_SERVERS 6 #define DHCP_OPT_HOSTNAME 12 #define DHCP_OPT_REQUESTED_ADDRESS 50 #define DHCP_OPT_LEASE_TIME 51 #define DHCP_OPT_MESSAGE_TYPE 53 #define DHCP_OPT_SERVER_IDENTIFIER 54 #define DHCP_OPT_PARAMETER_REQUEST_LIST 55 #define DHCP_OPT_RENEWAL_TIME 58 #define DHCP_OPT_CLIENT_IDENTIFIER 61 #define DHCP_OPT_END 255 #define DHCP_HTYPE_ETHER 1 static void send_dhcp_message(uint8_t *requestip) { memset(gPB, 0, UDP_DATA_P + sizeof( DHCPdata )); EtherCard::udpPrepare(DHCP_CLIENT_PORT, (dhcpState == DHCP_STATE_BOUND ? EtherCard::dhcpip : allOnes), DHCP_SERVER_PORT); // If we ever don't do this, the DHCP renewal gets sent to whatever random // destmacaddr was used by other code. Rather than cache the MAC address of // the DHCP server, just force a broadcast here in all cases. EtherCard::copyMac(gPB + ETH_DST_MAC, allOnes); //force broadcast mac // Build DHCP Packet from buf[UDP_DATA_P] DHCPdata *dhcpPtr = (DHCPdata*) (gPB + UDP_DATA_P); dhcpPtr->op = DHCP_BOOTREQUEST; dhcpPtr->htype = 1; dhcpPtr->hlen = 6; dhcpPtr->xid = currentXid; if (dhcpState == DHCP_STATE_BOUND) { EtherCard::copyIp(dhcpPtr->ciaddr, EtherCard::myip); } EtherCard::copyMac(dhcpPtr->chaddr, EtherCard::mymac); // options defined as option, length, value bufPtr = gPB + UDP_DATA_P + sizeof( DHCPdata ); // DHCP magic cookie static const byte cookie[] PROGMEM = { 0x63,0x82,0x53,0x63 }; for (byte i = 0; i < sizeof(cookie); i++) addToBuf(pgm_read_byte(&cookie[i])); addToBuf(DHCP_OPT_MESSAGE_TYPE); // DHCP_STATE_SELECTING, DHCP_STATE_REQUESTING addToBuf(1); // Length addToBuf(dhcpState == DHCP_STATE_INIT ? DHCP_DISCOVER : DHCP_REQUEST); // Client Identifier Option, this is the client mac address addToBuf(DHCP_OPT_CLIENT_IDENTIFIER); addToBuf(1 + ETH_LEN); // Length (hardware type + client MAC) addToBuf(DHCP_HTYPE_ETHER); addBytes(ETH_LEN, EtherCard::mymac); if (hostname[0]) { addOption(DHCP_OPT_HOSTNAME, strlen(hostname), (byte*) hostname); } if (requestip != NULL) { addOption(DHCP_OPT_REQUESTED_ADDRESS, IP_LEN, requestip); addOption(DHCP_OPT_SERVER_IDENTIFIER, IP_LEN, EtherCard::dhcpip); } // Additional info in parameter list - minimal list for what we need byte len = 3; if (dhcpCustomOptionNum) len++; addToBuf(DHCP_OPT_PARAMETER_REQUEST_LIST); addToBuf(len); // Length addToBuf(DHCP_OPT_SUBNET_MASK); addToBuf(DHCP_OPT_ROUTERS); addToBuf(DHCP_OPT_DOMAIN_NAME_SERVERS); if (dhcpCustomOptionNum) addToBuf(dhcpCustomOptionNum); // Custom option addToBuf(DHCP_OPT_END); // packet size will be under 300 bytes EtherCard::udpTransmit((bufPtr - gPB) - UDP_DATA_P); } static void process_dhcp_offer(uint16_t len, uint8_t *offeredip) { // Map struct onto payload DHCPdata *dhcpPtr = (DHCPdata*) (gPB + UDP_DATA_P); // Offered IP address is in yiaddr EtherCard::copyIp(offeredip, dhcpPtr->yiaddr); // Search for the server IP byte *ptr = (byte*) (dhcpPtr + 1) + 4; do { byte option = *ptr++; byte optionLen = *ptr++; if (option == DHCP_OPT_SERVER_IDENTIFIER) { EtherCard::copyIp(EtherCard::dhcpip, ptr); break; } ptr += optionLen; } while (ptr < gPB + len); } static void process_dhcp_ack(uint16_t len) { // Map struct onto payload DHCPdata *dhcpPtr = (DHCPdata*) (gPB + UDP_DATA_P); // Allocated IP address is in yiaddr EtherCard::copyIp(EtherCard::myip, dhcpPtr->yiaddr); // Scan through variable length option list identifying options we want byte *ptr = (byte*) (dhcpPtr + 1) + 4; bool done = false; do { byte option = *ptr++; byte optionLen = *ptr++; switch (option) { case DHCP_OPT_SUBNET_MASK: EtherCard::copyIp(EtherCard::netmask, ptr); break; case DHCP_OPT_ROUTERS: EtherCard::copyIp(EtherCard::gwip, ptr); break; case DHCP_OPT_DOMAIN_NAME_SERVERS: EtherCard::copyIp(EtherCard::dnsip, ptr); break; case DHCP_OPT_LEASE_TIME: case DHCP_OPT_RENEWAL_TIME: leaseTime = 0; for (byte i = 0; i<4; i++) leaseTime = (leaseTime << 8) + ptr[i]; if (leaseTime != DHCP_INFINITE_LEASE) { leaseTime *= 1000; // milliseconds } break; case DHCP_OPT_END: done = true; break; default: { // Is is a custom configured option? if (dhcpCustomOptionCallback && option == dhcpCustomOptionNum) { dhcpCustomOptionCallback(option, ptr, optionLen); } } } ptr += optionLen; } while (!done && ptr < gPB + len); } static bool dhcp_received_message_type (uint16_t len, byte msgType) { // Map struct onto payload DHCPdata *dhcpPtr = (DHCPdata*) (gPB + UDP_DATA_P); if (len >= 70 && gPB[UDP_SRC_PORT_L_P] == DHCP_SERVER_PORT && dhcpPtr->xid == currentXid ) { byte *ptr = (byte*) (dhcpPtr + 1) + 4; do { byte option = *ptr++; byte optionLen = *ptr++; if(option == DHCP_OPT_MESSAGE_TYPE && *ptr == msgType ) { return true; } ptr += optionLen; } while (ptr < gPB + len); } return false; } static char toAsciiHex(byte b) { char c = b & 0x0f; c += (c <= 9) ? '0' : 'A'-10; return c; } bool EtherCard::dhcpSetup (const char *hname, bool fromRam) { // Use during setup, as this discards all incoming requests until it returns. // That shouldn't be a problem, because we don't have an IP-address yet. // Will try 60 secs to obtain DHCP-lease. using_dhcp = true; if(hname != NULL) { if(fromRam) { strncpy(hostname, hname, DHCP_HOSTNAME_MAX_LEN); } else { strncpy_P(hostname, hname, DHCP_HOSTNAME_MAX_LEN); } } else { // Set a unique hostname, use Arduino-?? with last octet of mac address hostname[strlen(hostname) - 2] = toAsciiHex(mymac[5] >> 4); // Appends mac to last 2 digits of the hostname hostname[strlen(hostname) - 1] = toAsciiHex(mymac[5]); // Even if it's smaller than the maximum } dhcpState = DHCP_STATE_INIT; uint16_t start = millis(); while (dhcpState != DHCP_STATE_BOUND && uint16_t(millis()) - start < 60000) { if (isLinkUp()) DhcpStateMachine(packetReceive()); } updateBroadcastAddress(); delaycnt = 0; return dhcpState == DHCP_STATE_BOUND ; } void EtherCard::dhcpAddOptionCallback(uint8_t option, DhcpOptionCallback callback) { dhcpCustomOptionNum = option; dhcpCustomOptionCallback = callback; } void EtherCard::DhcpStateMachine (uint16_t len) { #ifdef DHCPDEBUG if (dhcpState != DHCP_STATE_BOUND) { Serial.print(millis()); Serial.print(" State: "); } switch (dhcpState) { case DHCP_STATE_INIT: Serial.println("Init"); break; case DHCP_STATE_SELECTING: Serial.println("Selecting"); break; case DHCP_STATE_REQUESTING: Serial.println("Requesting"); break; case DHCP_STATE_RENEWING: Serial.println("Renew"); break; } #endif switch (dhcpState) { case DHCP_STATE_BOUND: //!@todo Due to millis() wrap-around, DHCP renewal may not work if leaseTime is larger than 49days if (leaseTime != DHCP_INFINITE_LEASE && millis() - leaseStart >= leaseTime) { send_dhcp_message(myip); dhcpState = DHCP_STATE_RENEWING; stateTimer = millis(); } break; case DHCP_STATE_INIT: currentXid = millis(); memset(myip,0,IP_LEN); // force ip 0.0.0.0 send_dhcp_message(NULL); enableBroadcast(true); //Temporarily enable broadcasts dhcpState = DHCP_STATE_SELECTING; stateTimer = millis(); break; case DHCP_STATE_SELECTING: if (dhcp_received_message_type(len, DHCP_OFFER)) { uint8_t offeredip[IP_LEN]; process_dhcp_offer(len, offeredip); send_dhcp_message(offeredip); dhcpState = DHCP_STATE_REQUESTING; stateTimer = millis(); } else { if (millis() - stateTimer > DHCP_REQUEST_TIMEOUT) { dhcpState = DHCP_STATE_INIT; } } break; case DHCP_STATE_REQUESTING: case DHCP_STATE_RENEWING: if (dhcp_received_message_type(len, DHCP_ACK)) { disableBroadcast(true); //Disable broadcast after temporary enable process_dhcp_ack(len); leaseStart = millis(); if (gwip[0] != 0) setGwIp(gwip); // why is this? because it initiates an arp request dhcpState = DHCP_STATE_BOUND; } else { if (millis() - stateTimer > DHCP_REQUEST_TIMEOUT) { dhcpState = DHCP_STATE_INIT; } } break; } }