diff --git a/02-ntp/ntp-client.c b/02-ntp/ntp-client.c index f936010..48ba2f5 100644 --- a/02-ntp/ntp-client.c +++ b/02-ntp/ntp-client.c @@ -335,10 +335,22 @@ void demonstrate_epoch_conversion(void) { * Use demonstrate_epoch_conversion() to verify your conversion logic */ void get_current_ntp_time(ntp_timestamp_t *ntp_ts){ + struct timeval tv; + struct timezone tz; + struct tm *today; + printf("get_current_ntp_time() - TO BE IMPLEMENTED\n"); + gettimeofday(&tv, &tz); + today = localtime(&tv.tv_sec); + uint64_t seconds = tv.tv_sec + NTP_EPOCH_OFFSET; + uint64_t fraction = (uint64_t)tv.tv_usec * NTP_FRACTION_SCALE / USEC_INCREMENTS; + + ntp_ts->seconds = (uint32_t)seconds; + ntp_ts->fraction = (uint32_t)fraction; + // DONE // TODO: Implement this function // Hint: Use gettimeofday(), convert epoch, scale microseconds - memset(ntp_ts, 0, sizeof(ntp_timestamp_t)); + // memset(ntp_ts, 0, sizeof(ntp_timestamp_t)); } //STUDENT TODO @@ -366,11 +378,27 @@ void get_current_ntp_time(ntp_timestamp_t *ntp_ts){ * ERROR HANDLING: * If conversion fails, use snprintf to write "INVALID_TIME" to buffer */ + + void ntp_time_to_string(const ntp_timestamp_t *ntp_ts, char *buffer, size_t buffer_size, int local) { - printf("ntp_time_to_string() - TO BE IMPLEMENTED\n"); + // printf("ntp_time_to_string() - TO BE IMPLEMENTED\n"); // TODO: Implement this function - // Hint: Convert NTP to Unix time, use localtime/gmtime, format with snprintf - snprintf(buffer, buffer_size, "TO BE IMPLEMENTED"); + time_t unix_time = (time_t)NTP_TO_UNIX_SECONDS(ntp_ts->seconds); + struct tm *tm_info; + + if (local == LOCAL_TIME) + tm_info = localtime(&unix_time); + else + tm_info = gmtime(&unix_time); + + snprintf(buffer, buffer_size, + "%04d-%02d-%02d %02d:%02d:%02d", + tm_info->tm_year + 1900, + tm_info->tm_mon + 1, + tm_info->tm_mday, + tm_info->tm_hour, + tm_info->tm_min, + tm_info->tm_sec); } //STUDENT TODO @@ -397,10 +425,8 @@ void ntp_time_to_string(const ntp_timestamp_t *ntp_ts, char *buffer, size_t buff * Use NTP_FRACTION_SCALE (2^32) constant for the division */ double ntp_time_to_double(const ntp_timestamp_t* timestamp) { - printf("ntp_time_to_double() - TO BE IMPLEMENTED\n"); - // TODO: Implement this function - // Hint: Convert both parts to double and add - return 0.0; + return (double)timestamp->seconds + + ((double)timestamp->fraction / NTP_FRACTION_SCALE); } //STUDENT TODO @@ -419,9 +445,12 @@ double ntp_time_to_double(const ntp_timestamp_t* timestamp) { * Output: "Transmit Time: 2025-09-15 13:36:14.541216 (Local Time)" */ void print_ntp_time(const ntp_timestamp_t *ts, const char* label, int local){ - printf("print_ntp_time() - TO BE IMPLEMENTED - %s\n", label); + // printf("print_ntp_time() - TO BE IMPLEMENTED - %s\n", label); // TODO: Implement this function // Hint: Use ntp_time_to_string and printf + char buff[TIME_BUFF_SIZE]; + ntp_time_to_string(ts, buff, sizeof(buff), local); + printf("%s: %s\n", label, buff); } /* @@ -449,9 +478,11 @@ void print_ntp_time(const ntp_timestamp_t *ts, const char* label, int local){ * Network protocols require consistent byte order across different architectures */ void ntp_ts_to_net(ntp_timestamp_t* timestamp){ - printf("ntp_ts_to_net() - TO BE IMPLEMENTED\n"); + //printf("ntp_ts_to_net() - TO BE IMPLEMENTED\n"); // TODO: Implement this function // Hint: Use htonl() on both seconds and fraction fields + timestamp->seconds = htonl(timestamp->seconds); + timestamp->fraction = htonl(timestamp->fraction); } //STUDENT TODO @@ -473,9 +504,10 @@ void ntp_ts_to_net(ntp_timestamp_t* timestamp){ * Host processing requires native byte order for correct arithmetic */ void ntp_ts_to_host(ntp_timestamp_t* timestamp){ - printf("ntp_ts_to_host() - TO BE IMPLEMENTED\n"); // TODO: Implement this function // Hint: Use ntohl() on both seconds and fraction fields + timestamp->seconds = ntohl(timestamp->seconds); + timestamp->fraction = ntohl(timestamp->fraction); } //STUDENT TODO @@ -498,9 +530,17 @@ void ntp_ts_to_host(ntp_timestamp_t* timestamp){ * CALL THIS: Before sending packet over network */ void ntp_to_net(ntp_packet_t* packet){ - printf("ntp_to_net() - TO BE IMPLEMENTED\n"); + //printf("ntp_to_net() - TO BE IMPLEMENTED\n"); // TODO: Implement this function // Hint: Convert 32-bit fields with htonl(), timestamps with ntp_ts_to_net() + packet->root_delay = htonl(packet->root_delay); + packet->root_dispersion = htonl(packet->root_dispersion); + packet->reference_id = htonl(packet->reference_id); + + ntp_ts_to_net(&packet->ref_time); + ntp_ts_to_net(&packet->orig_time); + ntp_ts_to_net(&packet->recv_time); + ntp_ts_to_net(&packet->xmit_time); } //STUDENT TODO @@ -522,9 +562,16 @@ void ntp_to_net(ntp_packet_t* packet){ * CALL THIS: After receiving packet from network */ void ntp_to_host(ntp_packet_t* packet){ - printf("ntp_to_host() - TO BE IMPLEMENTED\n"); // TODO: Implement this function // Hint: Convert 32-bit fields with ntohl(), timestamps with ntp_ts_to_host() + packet->root_delay = ntohl(packet->root_delay); + packet->root_dispersion = ntohl(packet->root_dispersion); + packet->reference_id = ntohl(packet->reference_id); + + ntp_ts_to_host(&packet->ref_time); + ntp_ts_to_host(&packet->orig_time); + ntp_ts_to_host(&packet->recv_time); + ntp_ts_to_host(&packet->xmit_time); } /* @@ -575,9 +622,18 @@ int build_ntp_request(ntp_packet_t* packet) { return RC_BAD_PACKET; } memset(packet, 0, sizeof(ntp_packet_t)); - + SET_NTP_LI_VN_MODE(packet, + NTP_LI_UNSYNC, + NTP_VERSION, + NTP_MODE_CLIENT); + + packet->stratum = 0; + packet->poll = 6; + packet->precision = -20; + + get_current_ntp_time(&packet->xmit_time); // After you implement this, uncomment the line below to debug: - // debug_print_bit_fields(packet); + debug_print_bit_fields(packet); return RC_OK; } @@ -622,10 +678,28 @@ int build_ntp_request(ntp_packet_t* packet) { * RC_OK (0) on success, RC_BUFF_TOO_SMALL (-2) if buffer too small */ int decode_reference_id(uint8_t stratum, uint32_t ref_id, char *buff, int buff_sz){ - printf("decode_reference_id() - TO BE IMPLEMENTED\n"); - // TODO: Implement this function - // Hint: Check buffer sizes, handle ref_id==0, stratum>=2 (IP), stratum<2 (ASCII) - snprintf(buff, buff_sz, "TO BE IMPLEMENTED"); + if (!buff || buff_sz < 5) + return RC_BUFF_TOO_SMALL; + + if (ref_id == 0) { + snprintf(buff, buff_sz, "NONE"); + return RC_OK; + } + + if (stratum < 2) { + char c[5]; + c[0] = (ref_id >> 24) & 0xFF; + c[1] = (ref_id >> 16) & 0xFF; + c[2] = (ref_id >> 8) & 0xFF; + c[3] = ref_id & 0xFF; + c[4] = '\0'; + snprintf(buff, buff_sz, "%s", c); + } else { + struct in_addr addr; + addr.s_addr = htonl(ref_id); + inet_ntop(AF_INET, &addr, buff, buff_sz); + } + return RC_OK; } @@ -703,18 +777,34 @@ int calculate_ntp_offset(const ntp_packet_t* request, printf("calculate_ntp_offset() - TO BE IMPLEMENTED\n"); // TODO: Implement this function // Hint: Extract T1-T4 timestamps, apply NTP formulas, calculate dispersion - if (!request || !response || !result) { - return -1; + if (!request || !response || !result || !recv_time) { + return RC_BAD_PACKET; } - + // Initialize result with dummy values result->delay = 0.0; result->offset = 0.0; result->final_dispersion = 0.0; memset(&result->server_time, 0, sizeof(ntp_timestamp_t)); memset(&result->client_time, 0, sizeof(ntp_timestamp_t)); - - return 0; + + double T1 = ntp_time_to_double(&request->xmit_time); + double T2 = ntp_time_to_double(&response->recv_time); + double T3 = ntp_time_to_double(&response->xmit_time); + double T4 = ntp_time_to_double(recv_time); + + result->delay = (T4 - T1) - (T3 - T2); + result->offset = ((T2 - T1) + (T3 - T4)) / 2.0; + + result->server_time = response->xmit_time; + result->client_time = *recv_time; + + result->final_dispersion = + GET_NTP_Q1616_TS(response->root_dispersion) + + (GET_NTP_Q1616_TS(response->root_delay) / 2.0) + + (result->delay / 2.0); + + return RC_OK; } /* @@ -758,9 +848,51 @@ int calculate_ntp_offset(const ntp_packet_t* request, * Transmit Time (T3): 2025-09-15 09:09:34.348244 (Local Time) */ void print_ntp_packet_info(const ntp_packet_t* packet, const char* label, int packet_type) { - printf("print_ntp_packet_info() - TO BE IMPLEMENTED - %s Packet\n", label); + //printf("print_ntp_packet_info() - TO BE IMPLEMENTED - %s Packet\n", label); // TODO: Implement this function // Hint: Use printf for fields, GET_NTP_* macros for bit fields, decode_reference_id, print_ntp_time + char refid_buff[64]; + + printf("--- %s Packet ---\n", label); + + printf("Leap Indicator: %d\n", GET_NTP_LI(packet)); + printf("Version: %d\n", GET_NTP_VN(packet)); + printf("Mode: %d\n", GET_NTP_MODE(packet)); + + printf("Stratum: %d\n", packet->stratum); + printf("Poll: %d\n", packet->poll); + printf("Precision: %d\n", packet->precision); + + decode_reference_id(packet->stratum, + packet->reference_id, + refid_buff, + sizeof(refid_buff)); + + printf("Reference ID: [0x%08x] %s\n", + packet->reference_id, + refid_buff); + + printf("Root Delay: %u\n", + GET_NTP_Q1616_SEC(packet->root_delay)); + + printf("Root Dispersion %u\n", + GET_NTP_Q1616_SEC(packet->root_dispersion)); + + print_ntp_time(&packet->ref_time, + "Reference Time", + LOCAL_TIME); + + print_ntp_time(&packet->orig_time, + "Original Time (T1)", + LOCAL_TIME); + + print_ntp_time(&packet->recv_time, + "Receive Time (T2)", + LOCAL_TIME); + + print_ntp_time(&packet->xmit_time, + "Transmit Time (T3)", + LOCAL_TIME); } //STUDENT TODO @@ -792,7 +924,39 @@ void print_ntp_results(const ntp_result_t* result) { char svr_time_buff[TIME_BUFF_SIZE]; char cli_time_buff[TIME_BUFF_SIZE]; - printf("print_ntp_results() - TO BE IMPLEMENTED\n"); + //printf("print_ntp_results() - TO BE IMPLEMENTED\n"); //Hint: Note that you really dont have to do much here other than // Print out data that is passed in teh result arguement -} \ No newline at end of file + ntp_time_to_string(&result->server_time, + svr_time_buff, + sizeof(svr_time_buff), + LOCAL_TIME); + + ntp_time_to_string(&result->client_time, + cli_time_buff, + sizeof(cli_time_buff), + LOCAL_TIME); + + printf("\n=== NTP Time Synchronization Results ===\n"); + + printf("Server: pool.ntp.org\n"); + + printf("Server Time: %s (local time)\n", svr_time_buff); + printf("Local Time: %s (local time)\n", cli_time_buff); + + printf("Round Trip Delay: %.6f\n\n", result->delay); + + printf("Time Offset: %.6f seconds\n", result->offset); + printf("Final dispersion %.6f\n\n", result->final_dispersion); + + double offset_ms = fabs(result->offset * 1000.0); + double disp_ms = result->final_dispersion * 1000.0; + + if (result->offset > 0) { + printf("Your clock is running BEHIND by %.2fms\n", offset_ms); + } else { + printf("Your clock is running AHEAD by %.2fms\n", offset_ms); + } + + printf("Your estimated time error will be +/- %.2fms\n", disp_ms); +} diff --git a/02-ntp/protocol-investigation.md b/02-ntp/protocol-investigation.md new file mode 100644 index 0000000..c2ae65e --- /dev/null +++ b/02-ntp/protocol-investigation.md @@ -0,0 +1,103 @@ +Investigation + +Section 1: + +1. The AI model I used was ChatGPT. + +2. Prompts I gave it: +- "What is the purpose of NTP?" NTP is Network Time Protocol. It is used to synchronize the clocks of computers over a network. It has been in use since the 1980s. It is accurate to the millisecond over the Internet, and accurate to the microsecond on local networks. +Computers do not keep perfect time. If two computers are communicating with unsynchronized clocks, then their logs and event orders won't line up. +NTP synchronizes clocks with a series of timestamps. +The Client sends a request. Then the NTP Server receives the request. The Server then replies. The Client receives the reply. Through this, the client calculates the offset and delay of its clock. + +- "How are offsets determined in NTP?" Offsets are calculated using the formula ((T2 - T1) + (T3 - T4))/2. + +- "Why is UDP used instead of TCP?" I learned that the focus of UDP is to measure time intervals and delays. It isn't going to modify or resend lost packets. TCP, on the other hand will retransmit lost segments, which will create more delays unrelated to the latency of the network. My understanding is that UDP works within the raw time delay of the +network, without working around it in any way, allowing NTP to measure these delays. + +- I asked "why network byte order conversions are necessary". This is because different machines use different byte orders when storing multi-byte integers. Network protocols use big-endian format as the standard, so if a machine uses little-endian format, the bytes would be interpreted completely wrong. + +3. The most confusing concept initially was how NTP servers could even provide a more accurate time to the client. + +4. I asked ChatGPT "how NTP servers could accurately keep track of time". It responded that NTP servers themselves are not perfect clocks. They too need a reference, in the form of higher stratum devices. The highest stratum devices can be atomic clocks, GPS, or radio time signals. Stratum 1 servers are NTP servers connected to Stratum 0 devices. Stratum 2 NTP servers synchronize with Stratum 1 servers over a network. Stratum 3 or above synchronize with Stratum 2. + +The AI used the analogy of a marching band, explaining that the Stratum 0 devices are like the conductor with the metronome. Stratum 1 are the first row of musicians, following directly from the conductor. Stratum 2 are the next row following the first row. Stratum 3 and above are the audience trying to clap or tap along. This analogy really helped paint how the accuracy of NTP servers is based on which Stratum they are. Moreover, it explains that NTP servers are not magically accurate all the time, dispelling my nebulous view of NTP servers. + +Section 2: + +The AWS 2014 Clock Drift: + +In August of 2014, Amazon Web Services experienced clock drift in many of its EC2 instances. EC2 is a cloud based virtual server service that allows users to run virtual machines in the cloud. Virtual machines act as though they have their own clock, but in reality are just using the clock of their host machine. Normally, the VM's clock is synched to its host machine's clock. Under heavy load, some of the VM's clocks were not properly or fully updated. The clock drift between servers lead to inconsistencies in data. DynamoDB uses timestamps for version control. With timestamps being out of order due to clock drift, newer data could be overwritten by older data that appeared to be the latest based on the inaccurate timestamps. DynamoDB also requires that the client's clock is close enough to the server's time. This led to many rejected client requests, as it appeared that their request was expired. This affected thousands of users for a few hours. + + +The system prioritized availability over consistency. AWS ensured that services would still be available even if nodes had skewed clocks. Logical clocks, on their own, could not have prevented this issue as real time based timestamps were required for authentification issues. This bug occurred because physical time was used as a coordination mechanism without any way of double checking its accuracy from DynamoDB. + + +Section 3: + +A. +Timestamps from different servers are not guaranteed to reflect accurate timestamps in reference to each other due to a variety of possible reasons including network delays and differences in hardware clock speeds. Despite Server B's timestamp being earlier, Server A's event could have actually happened earlier due to these differences, meaning it is impossible to know for certain what happened first purely through timestamps. + +B. +Lamport clocks are logical clocks used in distributed systems. They are used in order to order events without using physical clock time. The logical time of an event is determined if that event was caused by another even happening. If event B is caused by event A, then event A's logical time has an earlier timestamp than event B. This records the order of events without having to worry about clock drift. Lamport clocks work as such: Each process has a local counter "c". When a local event occurs in that process, "c" is incremented. "c" is included when a message is sent. When the message is received, "c" is set to max(local c, received c) + 1. + +Logical time can tell us the order at which events happened in a process. Logical time can not tell us the actual real world time it took for such events to happen, or how much time passed between events. We still need NTP because we still need to use real-world timestamps. Real time is important for logging which is essential to debugging. Authentification depends on the time interval between the requests. Scheduling events need to know real time in order to take place at the right real world time. + + + +Section 4: + +A. +1. CAP stands for Consistency Availability Partition tolerance. +Consistency: Every read receives the most recent write/ all nodes see the same data at the same time. +Availability: Every request receives a response, even if some nodes fail. +Partition tolerance: The system continues to operate correctly even if network communication failures occur. + +2. "You can only guarantee two of the three porperties at the same time". + +3. Real world examples: +Traditional bank databases (MySQL): Prioritize Consistency + Availability (CA) (assuming partitions are rare or controlled). + +Amazon DynamoDB: Prioritizes Availability + Partition tolerance (AP), allowing eventual consistency. + +DNS system: Prioritizes Availability + Partition tolerance (AP), because some stale data is acceptable while ensuring the system keeps responding. + + +4. +Strong Consistency systems benefit from tight clock synchronization to order transactions correctly and avoid anomalies. Systems that prioritize Availability and Partition tolerance can tolerate loose clocks, because some temporary inconsistency is acceptable. Essentially, the data will converge eventually. + +B. +Eventual consistency is a property of distributed systems where updates to a shared data item will propagate to all replicas over time, so that if no new updates occur, all replicas will eventually converge to the same value. The term eventual emphasizes that consistency is not immediate but guaranteed after some propagation delay. An example is DNS propagation. When you update a domain’s IP address, some DNS servers will still return the old address for a short period until all servers eventually update. + +NTP is eventually consistent. When you start an NTP client, your clock does not instantly reach the exact accurate time. Instead, the system gradually adjusts the clock toward the accurate time to avoid large jumps that could disrupt applications. Similarly, in an eventually consistent system, perfect clock synchronization is not always required, because over time all nodes will converge to the correct state or time. + +Section 5: + +A. +1. I ran my NTP client 3 times and the offset values I gained were as such: +Run 1: offset = +0.0042 s +Run 2: offset = +0.0033 s +Run 3: offset = +0.0037 s + +Average offset: +0.0037 s + +2. My NTP client provides eventual consistency. This is because it doesn't ensure that all clocks are always synchronized to each other. Clock drift and network delay offsets are slightly different each time. Over time, however, repeated synchronizations cause the clocks to converge to around the same time. + + +B. +1. My NTP client can not solve the logical ordering problem. NTP gives an estimate of real world time, but it doesn't know the logical order of events. Essentially, NTP doesn't care if event A has to occur before event B, and cannot distinguish that. + +2. My NTP client does provide an estimate of real world time and present them in a human readable way. This can be used for logging, debugging, and authentification. + +3. Physical clocks (NTP) are needed for real world timekeeping, timestamps, accurate logs, and coordinating clock-time based events. + + Logical clocks (Lamport) are needed for tracking the causal order of events. + + In a real distributed system, you typically need both physical and logical clocks to get the most accurate order of events. + + + + + + +