在数字化时代,网络协议构成了互联网通信的基石。SSL、TLS、HTTP和DNS是其中最关键的几种,它们确保了我们的数据安全传输、网页的正确显示以及域名的正常解析。
要理解这些协议,首先需要了解网络分层模型。SSL和TLS位于传输层之上,用于加密数据;HTTP是应用层协议,负责网页内容的传输;而DNS则在应用层与传输层之间,它将人类可读的域名转换为机器可读的IP地址。
详细分析各个协议
DNS(域名系统)的主要作用
域名系统(DNS)是互联网的一项关键服务,它负责将用户可读的域名(如www.example.com
)转换为网络设备可识别的IP地址(如192.0.2.1
)。这个过程被称为域名解析。没有DNS,我们就需要记住复杂且难以记忆的数值序列来访问网站,这就大大降低了互联网的可用性。
DNS查询过程
当用户尝试访问一个网站时,以下是一个典型的DNS查询过程:
-
首先,用户的设备(比如电脑或手机)上的应用程序(通常是网页浏览器)会检查本地缓存,看看是否已经有对应域名的IP地址记录。如果有,它就直接使用该IP地址。
-
如果本地缓存中没有记录,设备会向配置的本地DNS服务器(通常由互联网服务提供商提供)发起解析请求。
-
本地DNS服务器收到请求后,会先检查自己的缓存。如果找到相应的记录,它会直接返回结果给请求者。
-
如果本地DNS服务器没有相关信息,它就会进行递归查询。在递归查询中,本地DNS服务器会代表请求者向其他DNS服务器发起查询请求,直到得到最终结果为止。
-
迭代查询则不同,它是一步接一步的过程,每一步只提供下一级查询的线索。在这种情况下,本地DNS服务器会告诉请求者下一个应该查询的DNS服务器的地址,请求者接着向这个新的地址发送请求,如此循环直至达到最终的权威名称服务器。
-
权威名称服务器负责管理特定域的DNS记录,它对所请求的域名拥有权威的信息。当查询到达这里,权威服务器会返回所请求的域名对应的IP地址记录。
-
一旦本地DNS服务器从权威名称服务器获取到所需的IP地址,它会将这个信息缓存起来以供后续使用,并将结果返回给原始请求者。
-
最后,原始请求者的设备收到解析出的IP地址,并使用该地址连接到目标网站。
从上面信息了解到,DNS通过一种分布式数据库的方式工作,它将域名解析任务分散到全球众多的DNS服务器上。这些服务器相互协作,通过递归或迭代查询,最终将用户友好的域名转换为网络上的设备能够理解的IP地址。
HTTP(超文本传输协议)的主要作用
超文本传输协议(HTTP)是用于传输网页文档的核心技术,它基于请求/响应模型工作。HTTP允许客户端(通常是Web浏览器)向服务器发送请求,并接收包含网页内容的响应。这个协议是无状态的,意味着每次通信后不会留下记录,除非使用某些技术如Cookie或会话标识来维持状态。
HTTP在应用层上运行,通常通过TCP/IP协议在网络中传输数据,但也可以在其他协议上运行,只要它们能提供类似可靠的流传输服务。
HTTP请求/响应模型
HTTP通信遵循一个非常直接的模型:一方(客户端)发送请求,另一方(服务器)返回响应。
-
请求:由以下几部分组成:
- 请求行:包含HTTP方法(如GET、POST)、请求的URL和HTTP协议版本。
- 请求头:包含关于请求的附加信息,例如用户代理、接受的内容类型、语言等。
- 空行:用以分隔请求头和请求体。
- 请求体:对于一些请求方法(如POST、PUT),可能包含额外的数据。
-
响应:同样由以下几部分组成:
- 状态行:包含HTTP协议版本、状态码和状态消息。
- 响应头:包含关于响应的附加信息,例如内容类型、内容长度、服务器信息等。
- 空行:用以分隔响应头和响应体。
- 响应体:实际返回给客户端的数据,比如HTML文档、图片或其他资源。
HTTP方法和状态码
-
方法:
- GET:请求指定资源的表示形式。使用查询字符串可包含额外信息。
- POST:提交要处理的数据,比如表单内容。
- PUT:上传指定的资源替换目标资源。
- DELETE:删除指定的资源。
- HEAD:请求资源的头部信息,不返回实体主体。
- OPTIONS:描述目标资源的通信选项。
- CONNECT:建立网络隧道,转换为代理服务器。
- 还有其他不常用的方法,如TRACE、PATCH等。
-
状态码:
- 1xx(信息):请求已接收,继续处理。
- 2xx(成功):请求正常处理完毕。例如,200 OK 表示请求成功,204 No Content 表示请求成功但没有内容返回。
- 3xx(重定向):需要进一步操作以完成请求。例如,301 Moved Permanently 表示资源永久移动到新位置,304 Not Modified 表示资源未修改,可以使用缓存的副本。
- 4xx(客户端错误):请求有语法错误或请求无法实现。例如,400 Bad Request 表示服务器无法理解请求的语法,404 Not Found 表示找不到请求的资源。
- 5xx(服务器错误):服务器在处理请求的过程中发生了错误。例如,500 Internal Server Error 表示服务器内部错误,503 Service Unavailable 表示服务器目前无法处理请求。
总结来说,HTTP是Web通信的基础,它定义了客户端和服务器之间交换信息的格式和规则。了解HTTP的方法和状态码对于开发和维护网站和网络应用程序至关重要。
SSL/TLS(安全套接字层/传输层安全)
SSL和TLS用于加密客户端与服务器之间的通信,保障数据传输安全。
SSL(Secure Sockets Layer)和TLS(Transport Layer Security)是网络安全中非常重要的技术。它们通过综合运用对称加密、非对称加密、消息认证码等密码学技术,为互联网上的数据传输提供安全保障。具体来说,SSL和TLS的主要作用包括:
- 加密数据:防止敏感信息如登录凭证、交易细节等在传输过程中被未授权的第三方窃取;
- 数据完整性:确保数据在传输过程中没有被篡改;
- 身份验证:验证通信双方的身份,保证用户正在与真正的服务器进行交流。
SSL/TLS的握手过程涉及多个步骤,这个过程确定了加密参数,并在客户端与服务器之间建立了加密连接。具体如下:
- 客户端发起请求:客户端发送一个"client hello"消息到服务器,包含版本信息、加密套件列表、压缩算法和其他必要信息。此消息以明文形式发送。
- 服务器响应:服务器回应一个"server hello"消息,选择客户端提供的加密套件,并发送自己的数字证书及相关信息。
- 客户端验证:客户端接收服务器的证书,并对证书的有效性进行验证。如果需要,客户端也可能发送自己的证书给服务器进行双向验证。
- 密钥协商:双方根据协商的加密套件进行密钥交换。如果是使用RSA算法,服务器会用自己的私钥加密一个预备主秘钥,客户端用服务器的公钥解密得到主秘钥。
- 确立加密参数:使用从前面步骤得到的主秘钥,生成加密和MAC(消息认证码)密钥,这些密钥将用于后续的所有通信。
- 加密通信:双方确认无误后,开始使用协商好的加密参数进行加密通信。
了解HTTPS(HTTP over SSL/TLS)的结合方式
HTTPS,全称为HTTP Secure,也就是安全版的HTTP,它将HTTP协议与SSL/TLS协议结合起来,在互联网上提供加密的数据传输。具体结合方式如下:
- 应用层与传输层的安全封装:在标准的HTTP通信过程中,所有的数据传输都是明文形式的,这意味着数据可以被任何监听网络流量的人读取。而HTTPS通过在HTTP之上添加SSL/TLS协议,为数据提供了加密处理。这样即便是数据被截获,没有相应的密钥也无法解密数据内容。
- 保持HTTP的应用逻辑不变:在HTTPS的工作模式中,原有的HTTP请求和响应的处理逻辑并没有改变。只是在数据传输到TCP协议之前,先由SSL/TLS进行加密,确保了传输过程的安全性。这种分层的方法既保障了旧的HTTP应用无需修改即可升级到HTTPS,也符合了TCP/IP协议族的分层哲学。
- 使用安全的握手建立连接:当客户端与服务器进行HTTPS通信时,会首先建立一个SSL/TLS的握手,以确保双方的身份验证,并且确立加密参数和密钥。这个过程是整个加密通信的基础,并确保了后续所有传输的数据都能得到加密保护。
- 对外部资源的处理:现代网页通常包含多个外部资源,如图片、CSS和JavaScript文件等。在使用HTTPS时,即使是这些外部资源,浏览器也会分别建立安全的连接来获取它们,从而确保整个页面加载过程的数据安全。
所以,HTTPS的核心在于它在HTTP上增加了一层SSL/TLS协议,以实现数据的加密传输。这种机制不仅提高了数据传输的安全性,而且在很大程度上继承了原有HTTP协议的应用模型,使得从HTTP迁移到HTTPS的过程对于开发者和用户来说相对平滑。
网络协议深度解析:SSL、 TLS、HTTP和 DNS(C/C++代码实现)
#define PKT_PARSE_OK 0
#define PKT_PARSE_ERR -1
#define PKT_IPPROTO_ICMP 1 //ICMP
#define PKT_IPPROTO_TCP 6 //TCP
#define PKT_IPPROTO_UDP 17 //UDP
#pragma pack(1)
typedef struct {
uint8_t dest[6];
uint8_t src[6];
uint16_t type;
}PKT_ETH_HEADER_S;
///* IPv4 header */
typedef struct
{
uint8_t ver_ihl; /*Version (4 bits) + Internet header length (4 bits)*/
uint8_t tos; /*Type of service*/
uint16_t tlen; /*Total length*/
uint16_t identification; /*Identification*/
uint16_t flags_fo; /*Flags (3 bits) + Fragment offset (13 bits)*/
uint8_t ttl; /*Time to live*/
uint8_t proto; /*Protocol*/
uint16_t crc; /*Header checksum*/
uint32_t saddr; /*Source address*/
uint32_t daddr; /*Destination address*/
//uint32_t op_pad; /*Option + Padding*/
} PKT_IP_HEADER_S;
//
///* UDP header*/
typedef struct
{
uint16_t sport; /*Source port*/
uint16_t dport; /*Destination port*/
uint16_t len; /*Datagram length*/
uint16_t crc; /*Checksum*/
} PKT_UDP_HEADER_S;
//
///* TCP header*/
typedef struct
{
uint16_t sport; /*Source port*/
uint16_t dport; /*Destination port*/
uint32_t seq; /*Sn number*/
uint32_t ack; /*Ack number*/
uint16_t flags;
uint16_t win; /*Window size*/
uint16_t sum; /*Checksum*/
uint16_t urp; /*Urgent pointer*/
} PKT_TCP_HEADER_S;
typedef struct
{
uint32_t linkid;
uint32_t seq; //seq
uint32_t ack;//ack
uint32_t exp;//expire next seq
uint16_t flags;
uint16_t win;//Window size
uint16_t crc;//Checksum
uint16_t urp;//Urgent pointer
}PKT_TCP_S;
typedef struct
{
uint32_t linkid;
uint16_t crc; //Checksum
uint32_t len; //udp len with head
uint8_t* data;
}PKT_UDP_S;
typedef union
{
PKT_TCP_S tcp;
PKT_UDP_S udp;
}PKT_TRANS_LAYER_U;
typedef struct
{
uint32_t sip; //源ip
uint32_t dip; //目的ip
uint16_t sport; //源port
uint16_t dport; //目的port
uint8_t proto; //传输层协议PKT_IPPROTO_TCP,PKT_IPPROTO_UDP
uint8_t *peth_pkt; // 报文指针
uint8_t *pnet_pkt; //网络层指针,网络层起始
uint8_t *ptrans_pkt; //传输层指针,传输层起始
uint8_t *papp_pkt; //数据层指针,数据层起始
uint8_t *pmpls_pkt;
uint8_t *l2;
uint8_t *l3;
uint8_t *l4;
uint16_t pkt_len; //整个报文长度
uint16_t ethh_len; //2-2.5层头长度
uint16_t net_len; //ip数据长度,网络层+传输层+应用层
uint16_t trans_len; //传输层数据长度,传输层+应用层
uint16_t app_len; //应用层数据长度
uint16_t mpls_len; //应用层数据长度
uint16_t eth_pack_num; //2.5层封装层数
uint8_t vlan_flag;
uint8_t ipfrag_flag; //ip分片标记,1分片
uint8_t mpls_flag;
uint8_t ipv4_flag;
uint8_t ipv6_flag;
uint8_t icmp_flag;
uint16_t app_proto; //应用层协议类型
PKT_TRANS_LAYER_U trans_info; //tcp,udp信息
}PKT_INFO_S;
#pragma pack()
#define IP_SIP(p) ntohl(((PKT_IP_HEADER_S *)(p))->saddr) /*源IP*/
#define IP_DIP(p) ntohl(((PKT_IP_HEADER_S *)(p))->daddr) /*目的IP*/
#define IP_HLEN(p) ((((PKT_IP_HEADER_S *)(p))->ver_ihl & 0x0F) << 2) /*IP头长度*/
#define IP_PLEN(p) ntohs(((PKT_IP_HEADER_S *)(p))->tlen) /*IP包长度*/
#define IP_IDEN(p) ntohs(((PKT_IP_HEADER_S *)(p))->identification) /*IP包标识*/
#define IP_OFF(p) ((ntohs(((PKT_IP_HEADER_S *)(p))->flags_fo)&0x1FFF)<<3)
#define IP_MF(p) ((((PKT_IP_HEADER_S *)(p))->flags_fo&0x20)>>5)
#define UDP_SPORT(p) ntohs(((PKT_UDP_HEADER_S *)(p))->sport) /*Upd包源端口*/
#define UDP_DPORT(p) ntohs(((PKT_UDP_HEADER_S *)(p))->dport) /*Udp包目的端口*/
#define UDP_PLEN(p) ntohs(((PKT_UDP_HEADER_S *)(p))->len) /*Udp包的长度*/
#define UDP_HLEN (uint16_t)8
#define TCP_SN(p) ntohl(((PKT_TCP_HEADER_S*)(p))->seq)
#define TCP_ACK(p) ntohl(((PKT_TCP_HEADER_S*)(p))->ack)
#define TCP_HLEN(p) ((((PKT_TCP_HEADER_S*)(p))->flags&0x00F0)>>2)
#define TCP_SPORT(p) ntohs(((PKT_TCP_HEADER_S *)(p))->sport) /*Tcp包源端口*/
#define TCP_DPORT(p) ntohs(((PKT_TCP_HEADER_S *)(p))->dport) /*Tcp包目的端口*/
#define TCP_WIN(p) ntohs(((PKT_TCP_HEADER_S *)(p))->win)
#define TCP_SYN(p) (((PKT_TCP_HEADER_S*)(p))->flags&0x0200)
#define TCP_FIN(p) (((PKT_TCP_HEADER_S*)(p))->flags&0x0100)
#define TCP_RST(p) (((PKT_TCP_HEADER_S*)(p))->flags&0x0400)
#define TCP_ACKF(p) (((PKT_TCP_HEADER_S*)(p))->flags&0x1000)
struct http_header {
char *name, *value;
};
struct http_message {
char type;
char *method, *url;
char *protocol;
char *status, *line;
#define HTTP_HEADER_MAX 16
struct http_header headers[HTTP_HEADER_MAX];
char *body;
char connection_close;
char connection_keepalive;
char gzip_supported;
};
typedef enum {
DNS_UDP = 0,
DNS_TCP,
}DNS_TRANS_TYPE;
typedef struct {
DNS_TRANS_TYPE trans_type;
int requestID;//请求标识
int requestFlag;//请求标志
char* dns_queries_data;
unsigned short dns_queries_len;
char DNSname[DNS_DATA_LENGTH+1];//DNS名称
unsigned short requestType;//查询类型
unsigned short requestClass;//查询类
}DNS_REQUEST_INFO;
...
void CallBackPacket(char *data, int len)
{
...
if (pkt_info.proto == PKT_IPPROTO_TCP && pkt_info.dport == 443)
{
//printf("-------------Https Packet Start------------- \n");
char servername[128] = {0};
int namelen = 0;
err = ssl_clienthello_parser_servername(pkt_info.papp_pkt, pkt_info.app_len, servername, &namelen);
if (err != 0)
{
//printf(" Not Find ServerName.\n");
}
else
{
if (namelen >= 128)
return;
servername[namelen] = 0;
printf("[HTTPS] Find ServerName: %s, Sip:%d.%d.%d.%d Dip:%d.%d.%d.%d Sport:%d Dport:%d \n", servername,
pkt_info.sip >> 24 & 0xff, pkt_info.sip >> 16 & 0xff, pkt_info.sip >> 8 & 0xff, pkt_info.sip & 0xff,
pkt_info.dip>> 24 & 0xff, pkt_info.dip >> 16 & 0xff, pkt_info.dip >> 8 & 0xff, pkt_info.dip & 0xff,
pkt_info.sport, pkt_info.dport);
}
//printf("-------------Https Packet E n d------------- \n");
}
else if (pkt_info.proto == PKT_IPPROTO_UDP && pkt_info.dport == 53)
{
DNS_REQUEST_INFO dns_req = {0};
err = dns_parse(pkt_info.papp_pkt, pkt_info.app_len, &dns_req);
if (err == 0)
{
printf("[DNS] DNS ServerName: %s, Sip:%d.%d.%d.%d Dip:%d.%d.%d.%d Sport:%d Dport:%d \n", dns_req.DNSname,
pkt_info.sip >> 24 & 0xff, pkt_info.sip >> 16 & 0xff, pkt_info.sip >> 8 & 0xff, pkt_info.sip & 0xff,
pkt_info.dip>> 24 & 0xff, pkt_info.dip >> 16 & 0xff, pkt_info.dip >> 8 & 0xff, pkt_info.dip & 0xff,
pkt_info.sport, pkt_info.dport);
}
}
else if (pkt_info.proto == PKT_IPPROTO_TCP && pkt_info.dport == 80)
{
struct http_message http_msg = {0};
err = http_parser(pkt_info.papp_pkt, &http_msg, HTTP_REQUEST);
if (err == 0)
{
int i = 0;
char host[256] = {0};
for (; i < 16; i++)
{
if (http_msg.headers[i].name)
{
if (strncmp(http_msg.headers[i].name, "Host", 4) == 0)
{
strcpy(host, http_msg.headers[i].value);
int len = strlen(host);
if (host[len-1] == '\r')
host[len-1] = 0;
break;
}
}
}
printf("[HTTP] Method:%s Host:%s URL: %s, Sip:%d.%d.%d.%d Dip:%d.%d.%d.%d Sport:%d Dport:%d \n",
http_msg.method,
host,
http_msg.url,
pkt_info.sip >> 24 & 0xff, pkt_info.sip >> 16 & 0xff, pkt_info.sip >> 8 & 0xff, pkt_info.sip & 0xff,
pkt_info.dip>> 24 & 0xff, pkt_info.dip >> 16 & 0xff, pkt_info.dip >> 8 & 0xff, pkt_info.dip & 0xff,
pkt_info.sport, pkt_info.dport);
}
}
...
return;
}
...
int main(int argc, char *argv[])
{
...
fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if(fd<0)
{
printf("socket create err %d. \n", fd);
goto failed_2;
}
//PACKET_VERSION和 SO_BINDTODEVICE可以省略
#if 1
const int tpacket_version = TPACKET_V1;
/* set tpacket hdr version. */
ret = setsockopt(fd, SOL_PACKET, PACKET_VERSION, &tpacket_version, sizeof (int));
if(ret<0)
{
printf("setsockopt packet version err %d. \n", ret);
goto failed_2;
}
/* bind to device. */
ret = setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, intfName, strlen(intfName));
if(ret<0)
{
printf("setsockopt bind device error %d . \n", ret);
goto failed_2;
}
#endif
struct tpacket_req req;
#define PER_PACKET_SIZE 2048
const int BUFFER_SIZE = 1024*1024*16; //16MB的缓冲区
req.tp_block_size = 4096;
req.tp_block_nr = BUFFER_SIZE/req.tp_block_size;
req.tp_frame_size = PER_PACKET_SIZE;
req.tp_frame_nr = BUFFER_SIZE/req.tp_frame_size;
ret = setsockopt(fd, SOL_PACKET, PACKET_RX_RING, (void *)&req, sizeof(req));
if(ret<0)
{
printf("setsockopt rx ring error %d . \n", ret);
goto failed_2;
}
buff = (char *)mmap(0, BUFFER_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(buff == MAP_FAILED)
{
printf("mmap err. \n");
goto failed_2;
}
int nIndex=0, i=0;
while(1)
{
//这里在poll前先检查是否已经有报文被捕获了
struct tpacket_hdr* pHead = (struct tpacket_hdr*)(buff+ nIndex*PER_PACKET_SIZE);
//如果frame的状态已经为TP_STATUS_USER了,说明已经在poll前已经有一个数据包被捕获了,如果poll后不再有数据包被捕获,那么这个报文不会被处理,这就是所谓的竞争情况。
if(pHead->tp_status == TP_STATUS_USER)
goto process_packet;
//poll检测报文捕获
struct pollfd pfd;
pfd.fd = fd;
//pfd.events = POLLIN|POLLRDNORM|POLLERR;
pfd.events = POLLIN;
pfd.revents = 0;
ret = poll(&pfd, 1, -1);
if(ret<0)
{
perror("poll");
goto failed_1;
}
process_packet:
//尽力的去处理环形缓冲区中的数据frame,直到没有数据frame了
for(i=0; i<req.tp_frame_nr; i++)
{
struct tpacket_hdr* pHead = (struct tpacket_hdr*)(buff+ nIndex*PER_PACKET_SIZE);
//XXX: 由于frame都在一个环形缓冲区中,因此如果下一个frame中没有数据了,后面的frame也就没有frame了
if(pHead->tp_status == TP_STATUS_KERNEL)
break;
//处理数据frame
CallBackPacket((char*)pHead+pHead->tp_mac, pHead->tp_len);
//char* mac = (char*)pHead+pHead->tp_mac;
//printf("GetPacket,Len:%d,Mac:%2x-%2x-%2x-%2x-%2x-%2x\n", pHead->tp_len,
// mac[0],mac[1],mac[2],mac[3],mac[4],mac[5]);
//重新设置frame的状态为TP_STATUS_KERNEL
pHead->tp_len = 0;
pHead->tp_status = TP_STATUS_KERNEL;
//更新环形缓冲区的索引,指向下一个frame
nIndex++;
nIndex%=req.tp_frame_nr;
}
}
...
}
Makefile
CC = gcc
CFLAGS = -Wall -g
LIBS = -lpthread
all: main
main: main.o dns_parser.o http_parser.o packet_handle.o pkt_parse.o ssl_parser.o
$(CC) $(CFLAGS) -o main main.o dns_parser.o http_parser.o packet_handle.o pkt_parse.o ssl_parser.o $(LIBS)
main.o: main.c
$(CC) $(CFLAGS) -c main.c
dns_parser.o: dns_parser.c
$(CC) $(CFLAGS) -c dns_parser.c
http_parser.o: http_parser.c
$(CC) $(CFLAGS) -c http_parser.c
packet_handle.o: packet_handle.c
$(CC) $(CFLAGS) -c packet_handle.c
pkt_parse.o: pkt_parse.c
$(CC) $(CFLAGS) -c pkt_parse.c
ssl_parser.o: ssl_parser.c
$(CC) $(CFLAGS) -c ssl_parser.c
clean:
rm -f *.o main
运行结果:
If you need the complete source code, please add the WeChat number (c17865354792)
使用套接字接收网络数据包的示例。它创建了原始套接字,并将其绑定到指定的网络接口上。然后,它使用mmap()函数将一个环形缓冲区映射到内存中,以便存储捕获的数据包。接下来,它使用poll()函数检测是否有新的数据包到达,如果有,则处理这些数据包。在处理数据包时,它会调用CallBackPacket()函数处理数据包
总结
SSL(安全套接层)和TLS(传输层安全)是用于在互联网通信中提供安全和数据完整性的协议。它们位于传输层之上,确保数据在传输过程中被加密,以防止未经授权的访问和篡改。
HTTP(超文本传输协议)是一种应用层协议,用于在互联网上传输网页和其他资源。它定义了客户端和服务器之间的通信规则,确保网页内容能够正确地传输和显示。
DNS(域名系统)是一种将人类可读的域名转换为机器可读的IP地址的系统。它位于应用层和传输层之间,当用户在浏览器中输入一个域名时,DNS会将其解析为对应的IP地址,以便计算机能够找到并连接到目标服务器。
We also undertake the development of program requirements here. If necessary, please follow the WeChat official account 【程序猿编码】and contact me
参考:RFC 1035、RFC 2246、RFC 7235