blob: 865f6197a391fe9a091134fc00d53e427192ce1d [file] [log] [blame]
// SPDX-License-Identifier: BSD-2-Clause
/*
* Copyright (C) 2023 The Android Open Source Project
*/
#include <common.h>
#include <fastboot.h>
#include <net.h>
#include <net6.h>
#include <net/fastboot_tcp.h>
#include <net/tcp.h>
#include <net/tcp6.h>
static const u8 header_buffer_size_bytes = 8;
static const u8 handshake_length = 4;
static const uchar *handshake = "FB01";
static char command[FASTBOOT_COMMAND_LEN] = {0};
static char response[FASTBOOT_RESPONSE_LEN] = {0};
static uchar curr_header_buffer[header_buffer_size_bytes] = {0};
static u16 curr_sport;
static u16 curr_dport;
static u32 curr_tcp_seq_num;
static u32 curr_tcp_ack_num;
static u64 curr_chunk_size;
static u64 curr_chunk_downloaded;
static u64 curr_header_downloaded;
static unsigned int curr_request_len;
static bool is_ipv6;
static size_t ip_header_size;
static enum fastboot_tcp_state {
FASTBOOT_CLOSED,
FASTBOOT_CONNECTED,
FASTBOOT_DOWNLOADING,
FASTBOOT_DISCONNECTING
} state = FASTBOOT_CLOSED;
static int command_handled_id;
static bool command_handled_success;
static void fastboot_tcp_reset_state(void)
{
state = FASTBOOT_CLOSED;
memset(command, 0, FASTBOOT_COMMAND_LEN);
memset(response, 0, FASTBOOT_RESPONSE_LEN);
memset(curr_header_buffer, 0, header_buffer_size_bytes);
curr_sport = 0;
curr_dport = 0;
curr_tcp_seq_num = 0;
curr_tcp_ack_num = 0;
curr_request_len = 0;
curr_chunk_size = 0;
curr_chunk_downloaded = 0;
curr_header_downloaded = 0;
command_handled_id = 0;
command_handled_success = false;
}
static void fastboot_tcp_answer(u8 action, unsigned int len)
{
const u32 response_seq_num = curr_tcp_ack_num;
const u32 response_ack_num = curr_tcp_seq_num +
(curr_request_len > 0 ? curr_request_len : 1);
#if defined(CONFIG_IPV6)
if (is_ipv6) {
net_send_tcp_packet6(len, htons(curr_sport), htons(curr_dport),
action, response_seq_num, response_ack_num);
return;
}
#endif
net_send_tcp_packet(len, htons(curr_sport), htons(curr_dport),
action, response_seq_num, response_ack_num);
}
static void fastboot_tcp_reset(void)
{
fastboot_tcp_answer(TCP_RST, 0);
tcp_set_tcp_state(TCP_CLOSED);
state = FASTBOOT_CLOSED;
fastboot_tcp_reset_state();
}
static void fastboot_tcp_send_packet(u8 action, const uchar *data, unsigned int len)
{
uchar *pkt = net_get_async_tx_pkt_buf();
memset((void *)pkt, 0, PKTSIZE);
pkt += net_eth_hdr_size() + ip_header_size + TCP_HDR_SIZE + TCP_TSOPT_SIZE + 2;
memcpy(pkt, data, len);
fastboot_tcp_answer(action, len);
memset((void *)pkt, 0, PKTSIZE);
}
static void fastboot_tcp_send_message(const char *message, unsigned int len)
{
__be64 len_be = __cpu_to_be64(len);
uchar *pkt = net_get_async_tx_pkt_buf();
memset((void *)pkt, 0, PKTSIZE);
pkt += net_eth_hdr_size() + ip_header_size + TCP_HDR_SIZE + TCP_TSOPT_SIZE + 2;
// Put first 8 bytes as a big endian message length
memcpy(pkt, &len_be, 8);
pkt += 8;
memcpy(pkt, message, len);
fastboot_tcp_answer(TCP_ACK | TCP_PUSH, len + 8);
memset((void *)pkt, 0, PKTSIZE);
}
static void fastboot_tcp_handler(uchar *pkt, u16 dport, u16 sport,
u32 tcp_seq_num, u32 tcp_ack_num,
u8 action, unsigned int len)
{
int remains_to_download;
int fastboot_command_id;
u64 command_size;
bool has_data = len != 0;
u8 tcp_fin = action & TCP_FIN;
u8 tcp_push = action & TCP_PUSH;
curr_sport = sport;
curr_dport = dport;
curr_tcp_seq_num = tcp_seq_num;
curr_tcp_ack_num = tcp_ack_num;
curr_request_len = len;
switch (state) {
case FASTBOOT_CLOSED:
if (!tcp_push) {
fastboot_tcp_reset();
break;
}
if (len != handshake_length || memcmp(pkt, handshake, handshake_length) != 0) {
fastboot_tcp_reset();
break;
}
fastboot_tcp_send_packet(TCP_ACK | TCP_PUSH, handshake, handshake_length);
state = FASTBOOT_CONNECTED;
break;
case FASTBOOT_CONNECTED:
if (tcp_fin) {
fastboot_tcp_answer(TCP_FIN | TCP_ACK, 0);
state = FASTBOOT_DISCONNECTING;
break;
}
if (!tcp_push || !has_data) {
fastboot_tcp_reset();
break;
}
// First 8 bytes is big endian message length
command_size = __be64_to_cpu(*(u64 *)pkt);
len -= 8;
pkt += 8;
// Only single packet messages are supported ATM
if (len != command_size) {
fastboot_tcp_reset();
break;
}
fastboot_tcp_send_packet(TCP_ACK | TCP_PUSH, NULL, 0);
strlcpy(command, pkt, len + 1);
fastboot_command_id = fastboot_handle_command(command, response);
fastboot_tcp_send_message(response, strlen(response));
command_handled_id = fastboot_command_id;
command_handled_success = strncmp("OKAY", response, 4) == 0 ||
strncmp("DATA", response, 4) == 0;
if (fastboot_command_id == FASTBOOT_COMMAND_DOWNLOAD && command_handled_success)
state = FASTBOOT_DOWNLOADING;
break;
case FASTBOOT_DOWNLOADING:
if (tcp_fin) {
fastboot_tcp_answer(TCP_FIN | TCP_ACK, 0);
state = FASTBOOT_DISCONNECTING;
break;
}
if (!has_data) {
fastboot_tcp_reset();
break;
}
// The Fastboot TCP download payload consists of two distinct types of segments:
// 1. <header> - 8 bytes big endian header specifying incoming data size
// 2. <data> - actual content we're downloading
//
// The download traffic typically follows this pattern:
// <header(20mb)><data:20mb><header(1mb)><data:1mb><header(100mb)><data:100mb> etc
//
// However, the way TCP/Fastboot operates allows for the possibility of these
// headers and data segments being placed within different TCP packets without
// following any specific pattern.
//
// For instance, it is possible for one TCP packet to contain multiple segments
// like this:
// | <header(2mb)><data:2mb><header(1mb)> | <data:1mb><header(10mb)> | <data:10mb> |
// | 1 | 2 | 3 |
//
// Or these segments might be even not aligned with TCP packet boundaries,
// as shown here:
// | <header(2mb)><data: | 2mb><header(1mb)><data:1mb><header( | 10mb)><data:10mb> |
// | 1 | 2 | 3 |
while (len > 0) {
// reading data
remains_to_download = curr_chunk_size - curr_chunk_downloaded;
remains_to_download = remains_to_download <= len ? remains_to_download : len;
if (remains_to_download > 0) {
if (fastboot_data_download(pkt, remains_to_download, response)) {
printf("Fastboot downloading error. Data remain: %u received: %u\n",
fastboot_data_remaining(), remains_to_download);
fastboot_tcp_reset();
goto out;
}
pkt += remains_to_download;
len -= remains_to_download;
curr_chunk_downloaded += remains_to_download;
}
// reading header
remains_to_download = header_buffer_size_bytes - curr_header_downloaded;
remains_to_download = remains_to_download <= len ? remains_to_download : len;
if (remains_to_download > 0) {
memcpy(curr_header_buffer + curr_header_downloaded, pkt, remains_to_download);
pkt += remains_to_download;
len -= remains_to_download;
curr_header_downloaded += remains_to_download;
if (curr_header_downloaded == header_buffer_size_bytes) {
curr_chunk_size = __be64_to_cpu(*(u64 *)curr_header_buffer);
curr_chunk_downloaded = 0;
curr_header_downloaded = 0;
memset(curr_header_buffer, 0, header_buffer_size_bytes);
}
}
}
if (fastboot_data_remaining() > 0) {
fastboot_tcp_send_packet(TCP_ACK, NULL, 0);
} else {
fastboot_data_complete(response);
curr_chunk_size = 0;
curr_chunk_downloaded = 0;
state = FASTBOOT_CONNECTED;
fastboot_tcp_send_message(response, strlen(response));
}
break;
case FASTBOOT_DISCONNECTING:
if (command_handled_success) {
fastboot_handle_boot(command_handled_id, command_handled_success);
command_handled_id = 0;
command_handled_success = false;
}
if (tcp_push)
state = FASTBOOT_CLOSED;
break;
}
out:
memset(command, 0, FASTBOOT_COMMAND_LEN);
memset(response, 0, FASTBOOT_RESPONSE_LEN);
curr_sport = 0;
curr_dport = 0;
curr_tcp_seq_num = 0;
curr_tcp_ack_num = 0;
curr_request_len = 0;
}
static void fastboot_tcp_handler_ipv4(uchar *pkt, u16 dport,
struct in_addr sip, u16 sport,
u32 tcp_seq_num, u32 tcp_ack_num,
u8 action, unsigned int len)
{
is_ipv6 = false;
ip_header_size = IP_HDR_SIZE;
fastboot_tcp_handler(pkt, dport, sport,
tcp_seq_num, tcp_ack_num,
action, len);
}
#if defined(CONFIG_IPV6)
static void fastboot_tcp_handler_ipv6(uchar *pkt, u16 dport,
struct in6_addr sip, u16 sport,
u32 tcp_seq_num, u32 tcp_ack_num,
u8 action, unsigned int len)
{
is_ipv6 = true;
ip_header_size = IP6_HDR_SIZE;
fastboot_tcp_handler(pkt, dport, sport,
tcp_seq_num, tcp_ack_num,
action, len);
}
#endif
void fastboot_tcp_start_server(void)
{
fastboot_tcp_reset_state();
printf("Using %s device\n", eth_get_name());
printf("Listening for fastboot command on tcp %pI4\n", &net_ip);
tcp_set_tcp_handler(fastboot_tcp_handler_ipv4);
#if defined(CONFIG_IPV6)
printf("Listening for fastboot command on %pI6\n", &net_ip6);
net_set_tcp_handler6(fastboot_tcp_handler_ipv6);
#endif
}