POSA_LEAKSMS/firmware/common/ethernetif.c
유창욱 90f121e14c chore: import codebase with security hardening
SHT30 온습도 모니터링 시스템 전체 소스(서버 PHP, STM32 펌웨어, SQL, 테스트).
전체 코드리뷰에서 도출된 보안 하드닝 10건 반영:
- 요청 서명 HMAC-SHA256 전환(펌웨어 sig.c/서버 config.php/호스트 패리티 동시)
- 재전송 방어 + 기본 API_KEY fail-closed + 디바이스 문자열 정제(api/sensor_data.php)
- 오프라인 SMS 중복 발송 경합 제거(cron_heartbeat.php, 원자적 선점)
- CSV 수식 주입 방지(monthly_report.php), 감사로그 회전 락(retention_cleanup.php)
- 브루트포스 카운터 원자화(login.php), 예시 TOTP 비밀키 무효화, 마이그레이션 멱등화

_backup/(하드코딩 실 비밀값 포함)·config.local.php·런타임 상태는 .gitignore 제외.
2026-06-20 09:37:40 +09:00

446 lines
18 KiB
C

/* =============================================================================
* ethernetif.c - LwIP <-> STM32 HAL_ETH 글루 (STM32F407 + LAN8720 RMII)
*
* ST 공식 LwIP "ethernetif.c" 템플릿(STM32F4 + LAN8720)을 RTOS(NO_SYS=0)용으로
* 적응. 핵심 구성:
* - low_level_init(): HAL_ETH 핸들/MAC(STM32 UID 유도)/DMA 디스크립터/버퍼
* 설정 후 ETH 시작.
* - low_level_output(): pbuf 체인 -> HAL_ETH_Transmit (Tx 디스크립터).
* - ethernetif_input task: RX 인터럽트 -> 세마포어 -> 태스크가 프레임을 꺼내
* pbuf 로 복사 후 netif->input(=tcpip_input) 호출.
* - HAL_ETH_MspInit(): RMII GPIO/클럭/NVIC 설정.
* - PHY 링크 폴링 태스크: LAN8720 BSR 를 읽어 링크/속도/듀플렉스 통지.
*
* 디스크립터/버퍼는 일반 SRAM(0x2000xxxx) 에 두며 4바이트 정렬. ETH DMA 는
* CCM(0x10000000) 에 접근할 수 없으므로 CCM 배치 금지.
*
* 참고 핀맵(RMII, app/하드웨어 명세):
* REF_CLK PA1, MDIO PA2, MDC PC1, CRS_DV PA7,
* RXD0 PC4, RXD1 PC5, TXEN PB11, TXD0 PB12, TXD1 PB13
* LAN8720 PHY 주소 = 0 (TODO(hw) 보드에서 확인).
* ===========================================================================*/
#include "lwip/opt.h"
#include "lwip/timeouts.h"
#include "lwip/netif.h"
#include "lwip/netifapi.h" /* netifapi_netif_set_link_up/down: 코어락 직렬화(tcpip 스레드 외부에서 호출) */
#include "lwip/pbuf.h"
#include "lwip/stats.h"
#include "lwip/snmp.h"
#include "lwip/ethip6.h"
#include "netif/etharp.h"
#include "netif/ethernet.h"
#include "app_config.h"
#include "applog.h"
#include "stm32f4xx_hal.h"
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include <string.h>
/* ── 컴파일타임 상수 ────────────────────────────────────────────────────── */
/* LAN8720 PHY 주소(RMII 스트랩). 기본 0. TODO(hw): 보드 스트랩 핀으로 확정. */
#ifndef LAN8720_PHY_ADDRESS
#define LAN8720_PHY_ADDRESS 0x00U
#endif
/* DMA 디스크립터/버퍼 개수. RAM 절약 + 1 HTTPS 커넥션 처리에 충분.
* 각 버퍼는 ETH_RX_BUF_SIZE(=ETH_MAX_PACKET_SIZE, 1524) 바이트. */
#define ETH_RXBUFNB 4U
#define ETH_TXBUFNB 4U
/* 이더넷 프레임 처리 정의가 헤더(stm32f4xx_hal_eth.h)에 없을 수 있어 보강. */
#ifndef ETH_MAX_PACKET_SIZE
#define ETH_MAX_PACKET_SIZE 1524U
#endif
#ifndef ETH_RX_BUF_SIZE
#define ETH_RX_BUF_SIZE ETH_MAX_PACKET_SIZE
#endif
#ifndef ETH_TX_BUF_SIZE
#define ETH_TX_BUF_SIZE ETH_MAX_PACKET_SIZE
#endif
/* netif 식별자. */
#define IFNAME0 'e'
#define IFNAME1 'n'
/* PHY 링크 폴링 주기. */
#define PHY_LINK_POLL_MS 500U
/* ── DMA 디스크립터/버퍼 (4바이트 정렬, 일반 SRAM) ──────────────────────── */
/* ETH DMA 는 CCM 접근 불가 -> 링커가 .bss(일반 SRAM)에 두도록 한다.
* 일부 프로젝트는 별도 섹션(.RxDecripSection 등)에 배치하지만, 기본 .bss 면 충분. */
__attribute__((aligned(4))) static ETH_DMADescTypeDef DMARxDscrTab[ETH_RXBUFNB];
__attribute__((aligned(4))) static ETH_DMADescTypeDef DMATxDscrTab[ETH_TXBUFNB];
__attribute__((aligned(4))) static uint8_t Rx_Buff[ETH_RXBUFNB][ETH_RX_BUF_SIZE];
__attribute__((aligned(4))) static uint8_t Tx_Buff[ETH_TXBUFNB][ETH_TX_BUF_SIZE];
/* ── HAL ETH 핸들 + RTOS 동기 객체 ──────────────────────────────────────── */
static ETH_HandleTypeDef s_heth;
static SemaphoreHandle_t s_rx_sem; /* RX IRQ -> input 태스크 통지 */
static struct netif *s_netif; /* 콜백/태스크에서 참조 */
/* ── 전방 선언 ──────────────────────────────────────────────────────────── */
static void ethernetif_input_task(void *arg);
static void phy_link_task(void *arg);
static struct pbuf *low_level_input(struct netif *netif);
/* =============================================================================
* HAL MSP: RMII GPIO / 클럭 / NVIC
* HAL_ETH_Init() 내부에서 HAL 이 자동 호출한다.
* ===========================================================================*/
void HAL_ETH_MspInit(ETH_HandleTypeDef *heth)
{
GPIO_InitTypeDef gpio = {0};
if (heth->Instance != ETH) {
return;
}
/* GPIO 포트 클럭. */
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
/* SYSCFG 는 RMII/MII 선택에 필요. ETH 클럭(MAC + Tx + Rx)을 모두 켠다.
* STM32F4 HAL 의 표준 ETH 클럭 매크로명. */
__HAL_RCC_SYSCFG_CLK_ENABLE();
__HAL_RCC_ETHMAC_CLK_ENABLE();
__HAL_RCC_ETHMACTX_CLK_ENABLE();
__HAL_RCC_ETHMACRX_CLK_ENABLE();
/* RMII 인터페이스 선택(반드시 ETH 클럭 활성화 후, MAC reset 전).
* HAL_ETH_Init 의 MediaInterface=RMII 가 SYSCFG 를 설정하지만 명시적으로 보장. */
SYSCFG->PMC |= SYSCFG_PMC_MII_RMII_SEL; /* 1 = RMII */
/* 공통 RMII AF 설정 헬퍼. 모든 핀은 AF11_ETH, push-pull, very-high speed. */
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
gpio.Alternate = GPIO_AF11_ETH;
/* PORTA: PA1(REF_CLK), PA2(MDIO), PA7(CRS_DV) */
gpio.Pin = GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_7;
HAL_GPIO_Init(GPIOA, &gpio);
/* PORTB: PB11(TX_EN), PB12(TXD0), PB13(TXD1) */
gpio.Pin = GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13;
HAL_GPIO_Init(GPIOB, &gpio);
/* PORTC: PC1(MDC), PC4(RXD0), PC5(RXD1) */
gpio.Pin = GPIO_PIN_1 | GPIO_PIN_4 | GPIO_PIN_5;
HAL_GPIO_Init(GPIOC, &gpio);
/* ETH 전역 인터럽트 활성화(RX 완료 시 input 태스크 깨움). */
HAL_NVIC_SetPriority(ETH_IRQn, 7, 0); /* FreeRTOS 안전 우선순위 범위 내 */
HAL_NVIC_EnableIRQ(ETH_IRQn);
}
/* =============================================================================
* ETH 인터럽트: HAL 디스패치 -> RxCpltCallback 에서 세마포어 give
* ===========================================================================*/
void ETH_IRQHandler(void)
{
HAL_ETH_IRQHandler(&s_heth);
}
void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth)
{
(void)heth;
BaseType_t hp_woken = pdFALSE;
if (s_rx_sem != NULL) {
xSemaphoreGiveFromISR(s_rx_sem, &hp_woken);
}
portYIELD_FROM_ISR(hp_woken);
}
/* =============================================================================
* low_level_init: HAL_ETH 초기화 + MAC 주소 + DMA 디스크립터 + 시작
* ===========================================================================*/
static void mac_from_uid(uint8_t mac[6])
{
/* STM32 96bit 고유 ID 에서 MAC 유도. 로컬 관리(LAA) 비트 set, 멀티캐스트 clear. */
uint32_t u0 = HAL_GetUIDw0();
uint32_t u1 = HAL_GetUIDw1();
uint32_t u2 = HAL_GetUIDw2();
uint32_t mix = u0 ^ u1 ^ u2;
mac[0] = 0x02; /* locally administered, unicast */
mac[1] = (uint8_t)(u2 & 0xFF);
mac[2] = (uint8_t)((u1 >> 8) & 0xFF);
mac[3] = (uint8_t)((mix >> 16) & 0xFF);
mac[4] = (uint8_t)((mix >> 8) & 0xFF);
mac[5] = (uint8_t)(mix & 0xFF);
}
static void low_level_init(struct netif *netif)
{
static uint8_t mac[6];
mac_from_uid(mac);
s_heth.Instance = ETH;
s_heth.Init.MACAddr = mac;
s_heth.Init.AutoNegotiation= ETH_AUTONEGOTIATION_ENABLE;
s_heth.Init.Speed = ETH_SPEED_100M; /* AutoNeg 시 무시 */
s_heth.Init.DuplexMode = ETH_MODE_FULLDUPLEX; /* AutoNeg 시 무시 */
s_heth.Init.MediaInterface = ETH_MEDIA_INTERFACE_RMII;
s_heth.Init.RxMode = ETH_RXINTERRUPT_MODE; /* 인터럽트 기반 RX */
s_heth.Init.ChecksumMode = ETH_CHECKSUM_BY_SOFTWARE; /* lwipopts SW 체크섬과 일치 */
s_heth.Init.PhyAddress = LAN8720_PHY_ADDRESS;
if (HAL_ETH_Init(&s_heth) != HAL_OK) {
/* AutoNeg 실패(케이블 없음 등)도 HAL_TIMEOUT 을 줄 수 있음 -> 계속 진행하고
* PHY 폴링 태스크가 이후 링크를 처리하게 한다. */
LOGW("eth: HAL_ETH_Init 경고(링크 없음 가능)");
}
/* MAC 길이/타입. */
netif->hwaddr_len = ETH_HWADDR_LEN; /* = 6 */
memcpy(netif->hwaddr, mac, 6);
netif->mtu = 1500;
netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP;
/* 링크 업/다운은 PHY 폴링 태스크가 netif_set_link_up/down 으로 통지하므로
* 여기서 NETIF_FLAG_LINK_UP 플래그를 미리 세우지 않는다. */
/* DMA 디스크립터 체인 초기화(Rx/Tx). */
HAL_ETH_DMATxDescListInit(&s_heth, DMATxDscrTab, &Tx_Buff[0][0], ETH_TXBUFNB);
HAL_ETH_DMARxDescListInit(&s_heth, DMARxDscrTab, &Rx_Buff[0][0], ETH_RXBUFNB);
/* MAC/DMA 시작(Tx/Rx enable). */
if (HAL_ETH_Start(&s_heth) != HAL_OK) {
LOGE("eth: HAL_ETH_Start 실패");
}
LOGI("eth: MAC=%02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
/* =============================================================================
* low_level_output: pbuf 체인 -> HAL_ETH_Transmit
* ===========================================================================*/
static err_t low_level_output(struct netif *netif, struct pbuf *p)
{
(void)netif;
/* 현재 Tx 디스크립터의 버퍼로 pbuf 체인을 선형 복사.
* (lwipopts: LWIP_NETIF_TX_SINGLE_PBUF=1 이라 보통 단일 조각이지만 안전하게 순회) */
uint8_t *buf = (uint8_t *)(s_heth.TxDesc->Buffer1Addr);
if (buf == NULL) {
return ERR_BUF;
}
uint32_t framelen = 0;
for (struct pbuf *q = p; q != NULL; q = q->next) {
if (framelen + q->len > ETH_TX_BUF_SIZE) {
LOGW("eth: TX 프레임 과대(%lu)", (unsigned long)(framelen + q->len));
return ERR_BUF;
}
memcpy(&buf[framelen], q->payload, q->len);
framelen += q->len;
}
if (HAL_ETH_TransmitFrame(&s_heth, framelen) != HAL_OK) {
/* 디스크립터가 CPU 소유가 아니면(Tx underflow) 복구: 디스크립터 리셋. */
if ((s_heth.DMASR & ETH_DMASR_TUS) != (uint32_t)RESET) {
s_heth.Instance->DMASR = ETH_DMASR_TUS;
s_heth.Instance->DMATPDR = 0; /* Transmit Poll Demand 로 재개 */
}
LINK_STATS_INC(link.drop);
return ERR_IF;
}
LINK_STATS_INC(link.xmit);
return ERR_OK;
}
/* =============================================================================
* low_level_input: HAL_ETH RX 디스크립터 -> 새 pbuf
* ===========================================================================*/
static struct pbuf *low_level_input(struct netif *netif)
{
(void)netif;
struct pbuf *p = NULL;
/* 수신 프레임 1개 가져오기. 없으면 NULL. */
if (HAL_ETH_GetReceivedFrame_IT(&s_heth) != HAL_OK) {
return NULL;
}
uint32_t len = s_heth.RxFrameInfos.length;
uint8_t *src = (uint8_t *)s_heth.RxFrameInfos.buffer;
if (len > 0 && len <= ETH_RX_BUF_SIZE) {
p = pbuf_alloc(PBUF_RAW, (u16_t)len, PBUF_POOL);
if (p != NULL) {
/* pbuf 체인 각 조각으로 복사(POOL 버퍼가 분할될 수 있음). */
uint32_t copied = 0;
for (struct pbuf *q = p; q != NULL && copied < len; q = q->next) {
uint32_t n = q->len;
if (copied + n > len) n = len - copied;
memcpy(q->payload, &src[copied], n);
copied += n;
}
LINK_STATS_INC(link.recv);
} else {
LINK_STATS_INC(link.memerr);
LINK_STATS_INC(link.drop);
}
} else {
LINK_STATS_INC(link.lenerr);
LINK_STATS_INC(link.drop);
}
/* DMA 디스크립터를 다시 DMA 소유로 반환(여러 디스크립터에 걸친 프레임 포함). */
{
__IO ETH_DMADescTypeDef *dmarxdesc = s_heth.RxFrameInfos.FSRxDesc;
for (uint32_t i = 0; i < s_heth.RxFrameInfos.SegCount; i++) {
dmarxdesc->Status |= ETH_DMARXDESC_OWN;
dmarxdesc = (ETH_DMADescTypeDef *)(dmarxdesc->Buffer2NextDescAddr);
}
s_heth.RxFrameInfos.SegCount = 0;
}
/* RX 버퍼 언어베일러블(RBUS) 비트가 섰으면 RX 재개. */
if ((s_heth.Instance->DMASR & ETH_DMASR_RBUS) != (uint32_t)RESET) {
s_heth.Instance->DMASR = ETH_DMASR_RBUS;
s_heth.Instance->DMARPDR = 0; /* Receive Poll Demand */
}
return p;
}
/* =============================================================================
* ethernetif_input_task: RX 세마포어 대기 -> 가용 프레임 모두 input 으로 전달
* ===========================================================================*/
static void ethernetif_input_task(void *arg)
{
struct netif *netif = (struct netif *)arg;
for (;;) {
/* RX IRQ 통지 대기. 타임아웃을 둬서 IRQ 누락 시에도 주기적으로
* 디스크립터를 비워(폴링) 록업을 방지한다. 반환값과 무관하게 매 주기
* 가용 프레임을 모두 처리한다. */
(void)xSemaphoreTake(s_rx_sem, pdMS_TO_TICKS(100));
struct pbuf *p;
while ((p = low_level_input(netif)) != NULL) {
/* netif->input 은 netif_add 등록상 tcpip_input(=ethernet_input 경유). */
if (netif->input(p, netif) != ERR_OK) {
pbuf_free(p);
}
}
}
}
/* =============================================================================
* phy_link_task: LAN8720 BSR 폴링 -> 링크 업/다운 + 속도/듀플렉스 반영
* ===========================================================================*/
static void phy_link_task(void *arg)
{
struct netif *netif = (struct netif *)arg;
int prev_up = -1;
for (;;) {
uint32_t bsr = 0;
int link_up = 0;
if (HAL_ETH_ReadPHYRegister(&s_heth, PHY_BSR, &bsr) == HAL_OK) {
link_up = (bsr & PHY_LINKED_STATUS) ? 1 : 0;
}
if (link_up != prev_up) {
if (link_up) {
/* 자동협상 결과로 MAC 속도/듀플렉스 갱신 후 ETH 재시작. */
uint32_t sr = 0;
/* LAN8720 special control/status 레지스터(0x1F)에서 속도 추출.
* 비표준 매크로 회피 위해 BCR/BSR 기반 보수적 처리. */
if (HAL_ETH_ReadPHYRegister(&s_heth, PHY_SR, &sr) == HAL_OK) {
/* LAN8720 PHY_SR(0x1F): bit4=duplex(1=full), bit2..3=speed
* (10=10M,100=100M). 보드 PHY 별 상이 가능 -> TODO(hw) 확인. */
uint32_t speed = ((sr >> 2) & 0x03); /* 01=10,10=100 (LAN8720) */
uint32_t duplex = (sr >> 4) & 0x01;
s_heth.Init.DuplexMode = duplex ? ETH_MODE_FULLDUPLEX
: ETH_MODE_HALFDUPLEX;
s_heth.Init.Speed = (speed == 0x02) ? ETH_SPEED_100M
: ETH_SPEED_10M;
}
HAL_ETH_ConfigMAC(&s_heth, NULL);
HAL_ETH_Start(&s_heth);
/* LWIP_TCPIP_CORE_LOCKING=1: 이 태스크는 tcpip 스레드가 아니므로
* raw netif_set_link_up 을 직접 부르면 코어 상태 레이스가 난다.
* netifapi_* 로 호출하면 tcpip 스레드 문맥(코어락 보유)에서 실행되어
* 안전하다. (링크 콜백 net_link_cb 도 그 문맥에서 raw netif_set_up
* 을 호출하므로 중첩 락 데드락이 없다.) */
netifapi_netif_set_link_up(netif);
LOGI("eth: PHY 링크 업 (speed=%s)",
(s_heth.Init.Speed == ETH_SPEED_100M) ? "100M" : "10M");
} else {
HAL_ETH_Stop(&s_heth);
netifapi_netif_set_link_down(netif);
LOGW("eth: PHY 링크 다운");
}
prev_up = link_up;
}
vTaskDelay(pdMS_TO_TICKS(PHY_LINK_POLL_MS));
}
}
/* =============================================================================
* ethernetif_init: LwIP netif_add 콜백 (net.c 가 등록)
* ===========================================================================*/
err_t ethernetif_init(struct netif *netif)
{
LWIP_ASSERT("netif != NULL", (netif != NULL));
s_netif = netif;
#if LWIP_NETIF_HOSTNAME
netif->hostname = BOARD_DEVICE_ID;
#endif
netif->name[0] = IFNAME0;
netif->name[1] = IFNAME1;
/* IPv4 출력 = etharp_output, 링크 출력 = low_level_output.
* netif->input 은 netif_add 의 마지막 인자(tcpip_input)로 설정됨. */
netif->output = etharp_output;
netif->linkoutput = low_level_output;
MIB2_INIT_NETIF(netif, snmp_ifType_ethernet_csmacd, 100000000);
/* RX 통지 세마포어. */
s_rx_sem = xSemaphoreCreateBinary();
if (s_rx_sem == NULL) {
LOGE("eth: RX 세마포어 생성 실패");
return ERR_MEM;
}
/* HW 초기화(MAC/PHY/DMA/시작). */
low_level_init(netif);
/* RX 처리 태스크 + PHY 링크 폴링 태스크 기동.
* 우선순위/스택은 lwipopts.h 의 ETHIF_RX_* 사용. */
if (xTaskCreate(ethernetif_input_task, ETHIF_RX_THREAD_NAME,
ETHIF_RX_THREAD_STACKSIZE, netif,
ETHIF_RX_THREAD_PRIO, NULL) != pdPASS) {
LOGE("eth: RX 태스크 생성 실패");
return ERR_MEM;
}
if (xTaskCreate(phy_link_task, "eth_phy",
256, netif,
DEFAULT_THREAD_PRIO, NULL) != pdPASS) {
LOGE("eth: PHY 태스크 생성 실패");
return ERR_MEM;
}
return ERR_OK;
}