mirror of
https://github.com/martin-ger/esp_wifi_repeater.git
synced 2026-07-27 19:56:03 +00:00
mDNS added, fixes bug in AP ARP
This commit is contained in:
+418
@@ -0,0 +1,418 @@
|
||||
# L2 Bridging on the ESP8266 (`VARIANT=bridge`)
|
||||
|
||||
This document describes how the transparent layer-2 bridge between the
|
||||
upstream WiFi (STA interface) and the local soft-AP works. All bridge logic
|
||||
lives in `user/bridge.c`, compiled in only when `REPEATER_MODE` is defined
|
||||
(set by `user/user_config_bridge.h`). The default `make` build still uses
|
||||
NAPT; `make VARIANT=bridge` selects the bridge.
|
||||
|
||||
The goal of the bridge is to make AP-side clients appear on the same IP
|
||||
subnet as the upstream router. They get IPs from the upstream DHCP server,
|
||||
ARP normally, and are reachable both from upstream and from each other –
|
||||
all while a single ESP8266 radio is doing the work.
|
||||
|
||||
## Why this is non-trivial
|
||||
|
||||
A real Ethernet bridge moves frames between ports without rewriting them.
|
||||
A WiFi "bridge" cannot do that, because of an 802.11 hard rule:
|
||||
|
||||
> A frame transmitted by an AP on its BSS must carry the AP's own BSSID/MAC
|
||||
> as the source. A frame transmitted by a STA must carry the STA's MAC as
|
||||
> the source.
|
||||
|
||||
So we cannot just forward an AP-side client's frame upstream verbatim – the
|
||||
upstream AP would drop it. The bridge therefore rewrites L2 source MACs at
|
||||
both interfaces and uses two helpers to keep L3 reachability working
|
||||
correctly:
|
||||
|
||||
* **FDB** – a small forwarding database mapping `client IP → real client MAC`.
|
||||
Populated from observed traffic and DHCP. Used to translate the upstream's
|
||||
unicast frames (addressed to the AP MAC) back to the right client on the
|
||||
AP side.
|
||||
* **Proxy ARP** – on each interface, the bridge answers ARP requests on
|
||||
behalf of the other side using the local interface's own MAC, so neither
|
||||
side ever sees the foreign MAC of a peer it cannot reach directly.
|
||||
|
||||
A custom mini mDNS responder (~100 lines) is also included because the SDK's
|
||||
built-in `espconn_mdns_init` cannot be made to listen on the AP netif.
|
||||
|
||||
## Files and entry points
|
||||
|
||||
| Item | Where |
|
||||
|---|---|
|
||||
| Bridge implementation | `user/bridge.c` |
|
||||
| Public API | `user/bridge.h` (`bridge_init`, `bridge_show_fdb`) |
|
||||
| Activation flag | `REPEATER_MODE` in `user/user_config_bridge.h` |
|
||||
| Bridge bring-up | `user_main.c` – `bridge_init(sta_nif, ap_nif)` is called on STA connect |
|
||||
|
||||
`bridge_init` saves the original `input`, `output`, and `linkoutput` function
|
||||
pointers of both netifs, then replaces `input` and `output` with the bridge's
|
||||
hook functions. `linkoutput` is left untouched – the bridge calls the saved
|
||||
originals directly when it needs to inject a frame on the wire. The STA
|
||||
netif is also made the default route, NAPT is disabled on both netifs, and
|
||||
the soft-AP channel is forced to match the STA channel.
|
||||
|
||||
## Datapath overview
|
||||
|
||||
There are four hooks. Two are inputs (frames coming up from a radio), two
|
||||
are outputs (IP packets going down from the local lwip stack):
|
||||
|
||||
```
|
||||
upstream WiFi (STA) AP-side client
|
||||
│ ▲ ▲ │
|
||||
│ │ │ │
|
||||
▼ │ s_orig_lo_sta s_orig_lo_ap ▲
|
||||
bridge_input_sta ◄──── frame from STA radio │
|
||||
bridge_output_sta ───► IP packet, default route │
|
||||
│ │
|
||||
│ │
|
||||
▼ │
|
||||
local lwip stack │
|
||||
▲ │
|
||||
│ │
|
||||
▼ │
|
||||
bridge_output_ap ───► IP packet via AP netif │
|
||||
bridge_input_ap ◄──── frame from AP radio ────────┘
|
||||
```
|
||||
|
||||
The two `input` hooks always work on a copied pbuf `q`. They classify the
|
||||
frame, possibly forward a copy, possibly hand the original up to the local
|
||||
stack, and free `q` before returning.
|
||||
|
||||
## The FDB (forwarding database)
|
||||
|
||||
```c
|
||||
#define FDB_SIZE 16
|
||||
#define FDB_TTL_S 600
|
||||
typedef struct { uint32_t ip; uint8_t mac[6]; uint32_t expires_s; } fdb_entry_t;
|
||||
```
|
||||
|
||||
Sixteen entries, ten-minute TTL, replace the oldest on overflow. Lookups are
|
||||
linear; that is fine for a handful of clients on an ESP8266.
|
||||
|
||||
`fdb_insert` rejects three special cases:
|
||||
|
||||
```c
|
||||
if (ip == 0) return;
|
||||
if (s_sta_nif && ip == s_sta_nif->ip_addr.addr) return;
|
||||
if (s_ap_nif && ip == s_ap_nif->ip_addr.addr) return;
|
||||
```
|
||||
|
||||
The own-IP guards are important: without them an AP-side client that
|
||||
generates traffic with an unfortunate source IP, or a DHCP collision against
|
||||
the STA address, can poison the FDB and make the management IP look like
|
||||
it lives on a client MAC. After that, ICMP/TCP to the management IP gets
|
||||
forwarded to the wrong client instead of being delivered locally.
|
||||
|
||||
The bridge populates the FDB from three sources:
|
||||
|
||||
* Every IPv4 packet arriving on the AP side (`ip->src` → original `eth->src`),
|
||||
done **before** any early return.
|
||||
* Every ARP packet arriving on the AP side (`arp->spa` → original `eth->src`).
|
||||
* DHCP ACK snooping on the STA side (`yiaddr` → recovered original chaddr,
|
||||
with the DHCP lease time as the TTL).
|
||||
|
||||
It is consulted by `bridge_input_sta` to find the right destination MAC for
|
||||
unicast frames coming down from upstream, and by both output hooks to route
|
||||
IP packets generated by the local stack to the correct side.
|
||||
|
||||
## Normal IP traffic
|
||||
|
||||
### Downstream (AP client → upstream)
|
||||
|
||||
`bridge_input_ap` runs:
|
||||
|
||||
1. **FDB learning first.** The source IP/MAC pair is inserted into the FDB
|
||||
before any early-return check. This is critical for the management-IP
|
||||
reachability case (see ICMP example below).
|
||||
2. **Frames addressed to the AP MAC** that aren't broadcast/multicast are
|
||||
handed straight to `s_orig_input_ap` so the local lwip stack can process
|
||||
them. These are packets like ICMP echo to the management IP, or TCP to the
|
||||
web UI / console.
|
||||
3. **Anything else** is rewritten: `eth->src` is replaced with the STA MAC
|
||||
and the frame is sent upstream via `s_orig_lo_sta`. From the upstream
|
||||
AP's perspective, it sees a single STA (us) sending traffic – which
|
||||
satisfies the 802.11 rule.
|
||||
4. **Broadcast/multicast frames** are *also* passed to `s_orig_input_ap`
|
||||
after forwarding, so the local stack can react (e.g. DHCP, mDNS).
|
||||
|
||||
### Upstream (upstream → AP client)
|
||||
|
||||
`bridge_input_sta` runs:
|
||||
|
||||
1. Frames whose `eth->src` is the AP's own MAC are returned to the local
|
||||
stack untouched – we have no business looping our own transmits back.
|
||||
2. For unicast IP frames addressed to the STA MAC (us) but whose IP `dst`
|
||||
belongs to a known AP-side client (`fdb_lookup(ip->dst)` succeeds), the
|
||||
destination MAC is rewritten to that client's MAC, the source MAC to the
|
||||
AP MAC, and the frame is sent down via `s_orig_lo_ap`. The original `p`
|
||||
is dropped (return `ERR_OK` after `pbuf_free(p)`).
|
||||
3. Frames truly addressed to the STA MAC (e.g. responses to local stack's
|
||||
own connections) fall through to `s_orig_input_sta`.
|
||||
4. Broadcast and multicast IP frames are sent down to the AP side and also
|
||||
passed to the local stack.
|
||||
|
||||
### Locally-generated IP (ICMP reply, web UI, mDNS responder, etc.)
|
||||
|
||||
When the local lwip stack itself generates a packet, it goes through one of
|
||||
the output hooks depending on which netif lwip routed it through.
|
||||
|
||||
`bridge_output_sta(netif, p, ipaddr)`:
|
||||
|
||||
* If `fdb_lookup(ipaddr->addr)` returns a MAC, the destination is an AP-side
|
||||
client. We prepend an Ethernet header (dst = client MAC, src = AP MAC) and
|
||||
inject the frame via `s_orig_lo_ap`, bypassing lwip's ARP entirely – we
|
||||
already know the MAC.
|
||||
* If the destination is IPv4 multicast (224/4), we additionally craft the
|
||||
standard `01:00:5e:xx:xx:xx` MAC and copy the frame to the AP side via
|
||||
`s_orig_lo_ap` *before* letting `s_orig_output_sta` send it upstream.
|
||||
* Otherwise, hand off to `s_orig_output_sta` – upstream traffic from the
|
||||
local stack.
|
||||
|
||||
`bridge_output_ap(netif, p, ipaddr)` mirrors this for the AP netif. Without
|
||||
this hook, lwip would try to ARP-resolve the client IP on the AP netif – but
|
||||
the AP netif's IP/subnet (`172.31.255.1/24`) bears no relation to the
|
||||
upstream subnet, so ARP would fail and lwip would emit a useless gratuitous
|
||||
ARP for its own dummy address. Using the FDB instead, we go straight from
|
||||
"I have an IP" to "I have a MAC and a link to send on".
|
||||
|
||||
### Concrete example: pinging the management IP from an AP client
|
||||
|
||||
1. AP client sends `ARP who-has 192.168.178.104?` (the STA IP). This is
|
||||
broadcast.
|
||||
2. `bridge_input_ap` sees `is_bcast = true`. Early FDB-learn block runs and
|
||||
`client_IP → client_MAC` is inserted.
|
||||
3. The ARP block detects `arp->tpa == sta_nif->ip_addr.addr` and replies
|
||||
with `send_proxy_arp_reply` carrying the AP MAC. The client's ARP cache
|
||||
now has `192.168.178.104 → AP-MAC`.
|
||||
4. AP client sends `ICMP echo` with `eth->dst = AP-MAC`. `is_to_ap_mac` is
|
||||
true, so the bridge passes the frame straight to `s_orig_input_ap`. The
|
||||
FDB-learn block has already recorded the client.
|
||||
5. lwip generates the echo reply: `src = STA-IP`, `dst = client-IP`. Routing
|
||||
sends it via the default netif (STA), so `bridge_output_sta` is invoked.
|
||||
6. `fdb_lookup(client-IP)` finds the MAC (step 2's learn). The bridge
|
||||
prepends the Ethernet header and calls `s_orig_lo_ap`. Reply arrives at
|
||||
the client.
|
||||
|
||||
The first version of the bridge did the FDB insert *after* the
|
||||
`is_to_ap_mac` early return, which broke this case – the ICMP reply went
|
||||
out the STA interface to the upstream router instead.
|
||||
|
||||
## DHCP
|
||||
|
||||
DHCP is the trickiest protocol on the bridge: a client and the upstream
|
||||
DHCP server have to talk transparently across the rewriting layer, and we
|
||||
need the FDB pre-populated by the time we forward the ACK so unicast
|
||||
delivery can find the client.
|
||||
|
||||
Two small data structures support this:
|
||||
|
||||
* **XID map** – 8 entries, 30 s TTL, indexed by the DHCP transaction ID,
|
||||
storing the *real* client MAC. We use it to undo the chaddr rewrite when
|
||||
the reply arrives upstream.
|
||||
* **DHCP option 61 (Client-Identifier)** – appended to outgoing requests
|
||||
with the real client MAC, so the server can distinguish multiple AP-side
|
||||
clients (which all appear to share the STA's MAC after rewriting).
|
||||
|
||||
### Downstream DHCP request (client → upstream server)
|
||||
|
||||
In `bridge_input_ap`'s IP block, when `udp->dst_port == 67`:
|
||||
|
||||
`snoop_dhcp_request` runs on `q` (the bridge's working copy):
|
||||
|
||||
1. Save the client's real `chaddr` in a local; record `(xid, real_mac)` in
|
||||
the XID map.
|
||||
2. Rewrite `dhcp->chaddr` to the STA MAC. The DHCP server will see the
|
||||
request as coming from the STA's MAC.
|
||||
3. Set the broadcast flag (`dhcp->flags |= 0x8000`) so the server replies as
|
||||
a broadcast – important because we cannot easily intercept a unicast
|
||||
reply addressed to a MAC the server has never seen.
|
||||
4. If option 61 isn't already present, append `[61, 7, 1, real_mac]` and
|
||||
patch the IP `len`, UDP `len`, and IP checksum to reflect the 9 added
|
||||
bytes.
|
||||
5. Zero the UDP checksum (legal for IPv4 UDP).
|
||||
|
||||
The (modified) frame is then forwarded upstream by the surrounding code via
|
||||
`s_orig_lo_sta(s_sta_nif, q)`.
|
||||
|
||||
### Upstream DHCP reply (server → client)
|
||||
|
||||
In `bridge_input_sta`'s IP block, when `udp->src_port == 67 && udp->dst_port == 68`:
|
||||
|
||||
`snoop_dhcp_reply` runs on `q`:
|
||||
|
||||
1. Look up the original client MAC in the XID map by `dhcp->xid`. If found,
|
||||
rewrite `dhcp->chaddr` back to the real MAC and remember it as
|
||||
`chaddr_out`.
|
||||
2. If the DHCP option 53 message type is **ACK** and `yiaddr` is non-zero,
|
||||
call `fdb_insert(yiaddr, chaddr_out)`. The lease time from option 51 is
|
||||
used as the TTL (clamped to a sane range). The FDB now knows
|
||||
`assigned-IP → client-MAC` *before* we forward the frame.
|
||||
3. Zero the UDP checksum.
|
||||
|
||||
`bridge_input_sta` then sets the destination MAC to `chaddr_out`
|
||||
(`have_dhcp` path), rewrites `eth->src` to the AP MAC, and sends the frame
|
||||
to the AP via `s_orig_lo_ap`. The DHCP client on the AP side receives a
|
||||
properly addressed broadcast/unicast reply, and from then on its IP is
|
||||
already in the FDB so any subsequent unicast traffic from upstream finds it.
|
||||
|
||||
The AP-side DHCP server is explicitly not started in bridge mode (the call
|
||||
to `wifi_softap_dhcps_start` is guarded by `#ifndef REPEATER_MODE`). Only
|
||||
the upstream server hands out leases.
|
||||
|
||||
## ARP
|
||||
|
||||
ARP needs a different rewrite policy than IP because both sides have to
|
||||
believe the L3 peer is reachable at the local interface's MAC, never the
|
||||
foreign MAC.
|
||||
|
||||
### Downstream ARP (AP client → upstream)
|
||||
|
||||
In `bridge_input_ap`'s ARP block:
|
||||
|
||||
1. **Always learn** `arp->spa → original eth->src` into the FDB.
|
||||
2. If it's an `ARP request` and the target is the STA's own IP, send a
|
||||
**proxy ARP reply** to the requester carrying the AP MAC
|
||||
(`send_proxy_arp_reply(s_ap_nif, s_orig_lo_ap, …, sta_nif->ip_addr.addr)`).
|
||||
This is the management-IP reachability path.
|
||||
3. Otherwise, rewrite `arp->sha` to the STA MAC (so that upstream sees the
|
||||
STA as the sender of any ARP probe/announcement) and forward via
|
||||
`s_orig_lo_sta`.
|
||||
|
||||
### Upstream ARP (upstream → AP client)
|
||||
|
||||
In `bridge_input_sta`'s ARP block:
|
||||
|
||||
1. If it's an `ARP request` and the target IP is in the FDB, send a
|
||||
**proxy ARP reply** carrying the STA MAC. The upstream router learns
|
||||
"client X lives at the STA MAC" and addresses subsequent unicast frames
|
||||
to us. We then translate them back to the right AP-side MAC using the
|
||||
FDB on the way down.
|
||||
2. Otherwise, rewrite `arp->sha` to the AP MAC. If the target is in the FDB,
|
||||
forward unicast to the matching client; if it's broadcast, flood to AP.
|
||||
|
||||
### Why proxy ARP rather than transparent ARP
|
||||
|
||||
The 802.11 source-MAC rule again: if we forwarded an ARP reply with the
|
||||
true client MAC as `eth->src`, the upstream AP would drop it. We therefore
|
||||
*always* terminate ARP at the bridge and re-issue replies with the local
|
||||
side's MAC. This is the price of running a transparent IP bridge over a
|
||||
WiFi link.
|
||||
|
||||
### Self-IP guard
|
||||
|
||||
Because `bridge_input_ap` learns `arp->spa → eth->src` unconditionally, the
|
||||
own-IP guard in `fdb_insert` prevents a misbehaving client from poisoning
|
||||
the FDB by gratuitously announcing the STA's IP for itself. Without that
|
||||
guard, the management IP would resolve to a client MAC and become locally
|
||||
unreachable.
|
||||
|
||||
## mDNS (`224.0.0.251:5353`)
|
||||
|
||||
mDNS has two oddities on this platform:
|
||||
|
||||
* **Upstream mDNS works through the SDK.** `espconn_mdns_init` (called in
|
||||
`user_main.c`) registers `esp-wifi-repeater.local` as an A record and as
|
||||
a `_http._tcp` service, listening on the STA netif. When an upstream
|
||||
device queries, the SDK responds.
|
||||
* **The SDK's mDNS does not respond to AP-side queries**, even with IGMP
|
||||
joined on the AP netif and even when we deliver the frame manually to the
|
||||
STA netif's input function. The SDK filters on a WiFi-driver interface
|
||||
tag below lwip that we cannot fake. We worked around this with a small
|
||||
custom responder built into `bridge.c`.
|
||||
|
||||
### Downstream mDNS query (AP client → bridge / upstream)
|
||||
|
||||
In `bridge_input_ap`'s IP block, when `udp->dst_port == 5353`,
|
||||
`handle_ap_mdns_query` is called *before* the frame is forwarded upstream.
|
||||
It:
|
||||
|
||||
1. Validates the DNS header (QR=0, sane qdcount).
|
||||
2. Walks each question label-by-label, case-insensitively matching against
|
||||
`esp-wifi-repeater.local`.
|
||||
3. If a question matches and the qtype is A or ANY, builds a complete reply
|
||||
pbuf:
|
||||
* Ethernet: `dst = client MAC`, `src = AP MAC`, type = IPv4
|
||||
* IP: `src = STA IP`, `dst = client IP`, TTL 255, proto UDP
|
||||
* UDP: `src = 5353`, `dst = client UDP source port`, checksum 0
|
||||
* mDNS: QR=1, AA=1, single A answer, hostname.local, TTL 120,
|
||||
class IN with cache-flush bit, RDATA = STA IP
|
||||
4. Sends the reply directly via `s_orig_lo_ap` and frees the pbuf.
|
||||
|
||||
The query is *also* forwarded upstream by the surrounding code (via
|
||||
`s_orig_lo_sta`), which means upstream nodes still see it. So the AP-side
|
||||
client may receive responses from *both* our bridge responder and any
|
||||
upstream responders – which is exactly how mDNS expects to behave.
|
||||
|
||||
### Upstream mDNS
|
||||
|
||||
When an upstream device queries mDNS, the frame is multicast and arrives on
|
||||
the STA radio. `bridge_input_sta` sees `is_bcast = true`, copies the frame
|
||||
to the AP side via `s_orig_lo_ap`, and also passes it to `s_orig_input_sta`.
|
||||
The SDK's mDNS responder picks it up and replies.
|
||||
|
||||
The reply is generated by lwip and routed through `bridge_output_sta`. The
|
||||
multicast destination doesn't match any FDB entry, so the FDB lookup
|
||||
fails – but the multicast branch builds the `01:00:5e:00:00:fb` MAC, copies
|
||||
the frame to the AP side via `s_orig_lo_ap`, then lets the original
|
||||
`s_orig_output_sta` deliver the same packet upstream. Both sides receive
|
||||
the reply.
|
||||
|
||||
### Why we can't use the SDK responder for AP queries
|
||||
|
||||
We tried the obvious paths and none worked:
|
||||
|
||||
* Calling `s_orig_input_ap(p, ap_nif)` – lwip's `ip_input` rejects the
|
||||
multicast packet because the AP netif hasn't joined the group.
|
||||
* `igmp_joingroup(&ap_nif->ip_addr, &mdns_grp)` so the AP netif accepts the
|
||||
packet – the multicast is now accepted by lwip but the SDK's UDP listener
|
||||
silently doesn't deliver it. The SDK appears to bind its mDNS PCB to the
|
||||
STA netif via internal means.
|
||||
* Re-injecting the frame as if received on STA, by calling
|
||||
`s_orig_input_sta(p, sta_nif)` – still no response. The SDK's filter is
|
||||
below lwip and apparently keys on a tag set by the WiFi driver when the
|
||||
frame physically arrived.
|
||||
|
||||
Hence the small custom responder. The hostname is hard-coded to match
|
||||
`user_main.c`'s `mdns.host_name = "esp-wifi-repeater"`; if you change it
|
||||
there, change `MDNS_HOSTNAME` and `MDNS_HOSTNAME_LEN` in `bridge.c`
|
||||
accordingly.
|
||||
|
||||
## Channel lock and sleep
|
||||
|
||||
`bridge_init` forces the soft-AP onto the same channel as the STA
|
||||
(`wifi_softap_set_config` with `ap_cfg.channel = my_channel`) – an ESP8266
|
||||
has a single radio and cannot bridge if the two interfaces are tuned to
|
||||
different channels. It also disables WiFi modem sleep
|
||||
(`wifi_set_sleep_type(NONE_SLEEP_T)`) so the AP stays continuously
|
||||
available.
|
||||
|
||||
## Inspecting state at runtime
|
||||
|
||||
`bridge_show_fdb` (CLI command `show fdb` in the console) dumps the
|
||||
forwarding database with TTLs:
|
||||
|
||||
```
|
||||
Repeater FDB (IP -> Client MAC):
|
||||
192.168.178.110 -> aa:bb:cc:dd:ee:ff (expires in 583 s)
|
||||
192.168.178.112 -> 11:22:33:44:55:66 (expires in 411 s)
|
||||
```
|
||||
|
||||
If the FDB is empty when traffic should be flowing, no DHCP ACKs and no
|
||||
incoming traffic from any AP client have been seen yet – usually a sign
|
||||
that the AP is on the wrong channel or a client hasn't connected yet.
|
||||
|
||||
## Limitations
|
||||
|
||||
* Single radio, single channel – the bridge is bound to the STA's current
|
||||
upstream channel.
|
||||
* IPv4 only. IPv6 multicast (mDNS over `ff02::fb`, ICMPv6 ND) is not
|
||||
rewritten.
|
||||
* Maximum 16 concurrent clients (FDB size). Bump `FDB_SIZE` in `bridge.c`
|
||||
if you need more.
|
||||
* Only A queries for a single hard-coded hostname are answered locally on
|
||||
the AP side; SRV/PTR/TXT browsing for the bridge's own services from AP
|
||||
clients goes through whatever upstream responder picks up the relayed
|
||||
query.
|
||||
* The own STA/AP IPs are barred from the FDB. If you assign multiple IPs to
|
||||
the STA netif (uncommon on this SDK), only the primary one is protected.
|
||||
@@ -90,6 +90,11 @@ SDK_INCDIR := $(addprefix -I$(SDK_BASE)/,$(SDK_INCDIR))
|
||||
|
||||
SRC := $(foreach sdir,$(SRC_DIR),$(wildcard $(sdir)/*.c))
|
||||
OBJ := $(patsubst %.c,$(BUILD_BASE)/%.o,$(SRC))
|
||||
|
||||
# mdns.o is absent from liblwip_open_napt.a; compile from esp-open-lwip source
|
||||
MDNS_OBJ = $(BUILD_BASE)/mdns.o
|
||||
OBJ += $(MDNS_OBJ)
|
||||
|
||||
LIBS := $(addprefix -l,$(LIBS))
|
||||
APP_AR := $(addprefix $(BUILD_BASE)/,$(TARGET)_app.a)
|
||||
TARGET_OUT := $(addprefix $(BUILD_BASE)/,$(TARGET).out)
|
||||
@@ -154,7 +159,10 @@ $(APP_AR): $(OBJ)
|
||||
$(vecho) "AR $@"
|
||||
$(Q) $(AR) cru $@ $^
|
||||
|
||||
checkdirs: $(BUILD_DIR) $(FW_BASE)
|
||||
checkdirs: $(BUILD_BASE) $(BUILD_DIR) $(FW_BASE)
|
||||
|
||||
$(BUILD_BASE):
|
||||
$(Q) mkdir -p $@
|
||||
|
||||
$(BUILD_DIR):
|
||||
$(Q) mkdir -p $@
|
||||
@@ -181,4 +189,8 @@ clean:
|
||||
$(Q) rm -rf $(FW_BASE) $(BUILD_BASE)
|
||||
$(Q) find . -name "*~" -print0 | xargs -0 rm -rf
|
||||
|
||||
$(MDNS_OBJ): $(BUILD_AREA)/esp-open-sdk/esp-open-lwip/lwip/core/mdns.c | $(BUILD_BASE)
|
||||
$(vecho) "CC lwip/core/mdns.c"
|
||||
$(Q) $(CC) $(EXTRA_INCDIR) $(SDK_INCDIR) $(CFLAGS) -w -c $< -o $@
|
||||
|
||||
$(foreach bdir,$(BUILD_DIR),$(eval $(call compile-objects,$(bdir))))
|
||||
|
||||
+199
-29
@@ -34,7 +34,6 @@ static netif_output_fn s_orig_output_sta;
|
||||
static netif_output_fn s_orig_output_ap;
|
||||
static netif_linkoutput_fn s_orig_lo_sta;
|
||||
static netif_linkoutput_fn s_orig_lo_ap;
|
||||
static os_timer_t s_arp_timer;
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Compact packed header types
|
||||
@@ -134,6 +133,8 @@ static fdb_entry_t s_fdb[FDB_SIZE];
|
||||
static void ICACHE_FLASH_ATTR fdb_insert(uint32_t ip, const uint8_t *mac)
|
||||
{
|
||||
if (ip == 0) return;
|
||||
if (s_sta_nif && ip == s_sta_nif->ip_addr.addr) return;
|
||||
if (s_ap_nif && ip == s_ap_nif->ip_addr.addr) return;
|
||||
uint32_t now = now_secs();
|
||||
int free_idx = -1, oldest_idx = 0;
|
||||
uint32_t oldest_exp = 0xFFFFFFFFUL;
|
||||
@@ -256,28 +257,6 @@ static void ICACHE_FLASH_ATTR send_proxy_arp_reply(struct netif *tx_nif, netif_l
|
||||
lo(tx_nif, p); pbuf_free(p);
|
||||
}
|
||||
|
||||
static void ICACHE_FLASH_ATTR send_garp(uint32_t ip)
|
||||
{
|
||||
uint16_t pkt_len = sizeof(eth_hdr_t) + sizeof(arp_hdr_t);
|
||||
struct pbuf *p = pbuf_alloc(PBUF_RAW, pkt_len, PBUF_RAM);
|
||||
if (!p) return;
|
||||
eth_hdr_t *eth = (eth_hdr_t *)p->payload;
|
||||
arp_hdr_t *arp = (arp_hdr_t *)((uint8_t *)p->payload + sizeof(eth_hdr_t));
|
||||
os_memset(eth->dst, 0xff, 6); os_memcpy(eth->src, s_sta_nif->hwaddr, 6); eth->type = htons(ETHTYPE_ARP);
|
||||
arp->hwtype = htons(1); arp->prtype = htons(ETHTYPE_IP); arp->hwlen = 6; arp->prlen = 4;
|
||||
arp->op = htons(1); os_memcpy(arp->sha, s_sta_nif->hwaddr, 6); arp->spa = ip;
|
||||
os_memset(arp->tha, 0, 6); arp->tpa = ip;
|
||||
s_orig_lo_sta(s_sta_nif, p); pbuf_free(p);
|
||||
}
|
||||
|
||||
static void ICACHE_FLASH_ATTR arp_timer_func(void *arg)
|
||||
{
|
||||
uint32_t now = now_secs();
|
||||
int i;
|
||||
for (i = 0; i < FDB_SIZE; i++) {
|
||||
if (s_fdb[i].ip != 0 && s_fdb[i].expires_s > now) send_garp(s_fdb[i].ip);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* DHCP snooping
|
||||
@@ -348,6 +327,164 @@ static bool ICACHE_FLASH_ATTR snoop_dhcp_reply(struct pbuf *p, uint16_t eth_ip_u
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Minimal mDNS responder for AP-side queries
|
||||
*
|
||||
* The SDK's espconn_mdns_init only handles queries arriving on the STA netif
|
||||
* (it filters on a WiFi-driver interface tag below lwip), so AP-side clients
|
||||
* cannot resolve esp-wifi-repeater.local through it. We answer A queries for
|
||||
* the configured hostname directly here, sending a unicast reply to the
|
||||
* querier's MAC via the AP linkoutput.
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
#define MDNS_HOSTNAME "esp-wifi-repeater"
|
||||
#define MDNS_HOSTNAME_LEN 17
|
||||
|
||||
typedef struct {
|
||||
uint16_t id;
|
||||
uint16_t flags;
|
||||
uint16_t qdcount;
|
||||
uint16_t ancount;
|
||||
uint16_t nscount;
|
||||
uint16_t arcount;
|
||||
} __attribute__((packed)) dns_hdr_t;
|
||||
|
||||
static inline uint8_t ICACHE_FLASH_ATTR ascii_lower(uint8_t c)
|
||||
{
|
||||
return (c >= 'A' && c <= 'Z') ? (uint8_t)(c + 32) : c;
|
||||
}
|
||||
|
||||
static bool ICACHE_FLASH_ATTR label_iequal(const uint8_t *a, const char *b, uint8_t len)
|
||||
{
|
||||
uint8_t i;
|
||||
for (i = 0; i < len; i++)
|
||||
if (ascii_lower(a[i]) != ascii_lower((uint8_t)b[i])) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Walk a single question name starting at body[*pos]. Returns true if it
|
||||
matches "<hostname>.local" (case-insensitive). On return *pos points past
|
||||
the name and qtype/qclass are filled in. Compression pointers in questions
|
||||
are rare and not handled. */
|
||||
static bool ICACHE_FLASH_ATTR mdns_match_hostname(const uint8_t *body, uint16_t body_len,
|
||||
uint16_t *pos, uint16_t *qtype, uint16_t *qclass)
|
||||
{
|
||||
uint16_t p = *pos;
|
||||
bool match = true;
|
||||
uint8_t label_idx = 0;
|
||||
while (p < body_len) {
|
||||
uint8_t len = body[p++];
|
||||
if (len == 0) break;
|
||||
if ((len & 0xC0) != 0) return false;
|
||||
if (p + len > body_len) return false;
|
||||
if (label_idx == 0) {
|
||||
if (len != MDNS_HOSTNAME_LEN || !label_iequal(body + p, MDNS_HOSTNAME, len)) match = false;
|
||||
} else if (label_idx == 1) {
|
||||
if (len != 5 || !label_iequal(body + p, "local", len)) match = false;
|
||||
} else {
|
||||
match = false;
|
||||
}
|
||||
p += len;
|
||||
label_idx++;
|
||||
}
|
||||
if (label_idx != 2) match = false;
|
||||
if (p + 4 > body_len) return false;
|
||||
*qtype = ((uint16_t)body[p] << 8) | body[p + 1];
|
||||
*qclass = ((uint16_t)body[p + 2] << 8) | body[p + 3];
|
||||
*pos = p + 4;
|
||||
return match;
|
||||
}
|
||||
|
||||
static void ICACHE_FLASH_ATTR send_mdns_a_reply(const uint8_t *client_mac, uint32_t client_ip,
|
||||
uint16_t client_udp_port, uint16_t query_id)
|
||||
{
|
||||
if (s_sta_nif->ip_addr.addr == 0) return;
|
||||
|
||||
const uint16_t name_len = 1 + MDNS_HOSTNAME_LEN + 1 + 5 + 1; /* 25 */
|
||||
const uint16_t answer_len = name_len + 2 + 2 + 4 + 2 + 4; /* +type,class,ttl,rdlen,rdata */
|
||||
const uint16_t mdns_len = sizeof(dns_hdr_t) + answer_len;
|
||||
const uint16_t udp_len = sizeof(udp_hdr_t) + mdns_len;
|
||||
const uint16_t ip_len = sizeof(ip_hdr_t) + udp_len;
|
||||
const uint16_t pkt_len = sizeof(eth_hdr_t) + ip_len;
|
||||
|
||||
struct pbuf *p = pbuf_alloc(PBUF_RAW, pkt_len, PBUF_RAM);
|
||||
if (!p) return;
|
||||
|
||||
eth_hdr_t *eth = (eth_hdr_t *)p->payload;
|
||||
os_memcpy(eth->dst, client_mac, 6);
|
||||
os_memcpy(eth->src, s_ap_nif->hwaddr, 6);
|
||||
eth->type = htons(ETHTYPE_IP);
|
||||
|
||||
ip_hdr_t *ip = (ip_hdr_t *)((uint8_t *)p->payload + sizeof(eth_hdr_t));
|
||||
ip->vhl = 0x45; ip->tos = 0; ip->len = htons(ip_len);
|
||||
ip->id = 0; ip->off = 0; ip->ttl = 255; ip->proto = 17; ip->chksum = 0;
|
||||
ip->src = s_sta_nif->ip_addr.addr;
|
||||
ip->dst = client_ip;
|
||||
update_ip_chksum(ip);
|
||||
|
||||
udp_hdr_t *udp = (udp_hdr_t *)((uint8_t *)ip + sizeof(ip_hdr_t));
|
||||
udp->src_port = htons(5353);
|
||||
udp->dst_port = htons(client_udp_port);
|
||||
udp->len = htons(udp_len);
|
||||
udp->chksum = 0;
|
||||
|
||||
dns_hdr_t *dns = (dns_hdr_t *)((uint8_t *)udp + sizeof(udp_hdr_t));
|
||||
dns->id = htons(query_id);
|
||||
dns->flags = htons(0x8400); /* QR=1, AA=1 */
|
||||
dns->qdcount = 0; dns->ancount = htons(1); dns->nscount = 0; dns->arcount = 0;
|
||||
|
||||
uint8_t *a = (uint8_t *)dns + sizeof(dns_hdr_t);
|
||||
*a++ = MDNS_HOSTNAME_LEN; os_memcpy(a, MDNS_HOSTNAME, MDNS_HOSTNAME_LEN); a += MDNS_HOSTNAME_LEN;
|
||||
*a++ = 5; os_memcpy(a, "local", 5); a += 5;
|
||||
*a++ = 0;
|
||||
*a++ = 0x00; *a++ = 0x01; /* TYPE = A */
|
||||
*a++ = 0x80; *a++ = 0x01; /* CLASS = IN with cache-flush bit */
|
||||
*a++ = 0; *a++ = 0; *a++ = 0; *a++ = 120; /* TTL = 120s */
|
||||
*a++ = 0; *a++ = 4; /* RDLENGTH = 4 */
|
||||
os_memcpy(a, &s_sta_nif->ip_addr.addr, 4);
|
||||
|
||||
s_orig_lo_ap(s_ap_nif, p);
|
||||
pbuf_free(p);
|
||||
}
|
||||
|
||||
/* Called for an mDNS query (UDP dst port 5353) arriving on the AP side.
|
||||
q is the bridge's working copy of the frame; udp_off is the offset of the
|
||||
UDP header from the start of the Ethernet frame. client_mac is the
|
||||
original src MAC from the unrewritten Ethernet header. */
|
||||
static void ICACHE_FLASH_ATTR handle_ap_mdns_query(struct pbuf *q, uint16_t udp_off, const uint8_t *client_mac)
|
||||
{
|
||||
udp_hdr_t *udp = (udp_hdr_t *)pkt_at(q, udp_off, sizeof(udp_hdr_t));
|
||||
if (!udp) return;
|
||||
uint16_t mdns_off = udp_off + sizeof(udp_hdr_t);
|
||||
dns_hdr_t *dns = (dns_hdr_t *)pkt_at(q, mdns_off, sizeof(dns_hdr_t));
|
||||
if (!dns) return;
|
||||
|
||||
if (ntohs(dns->flags) & 0x8000) return; /* not a query */
|
||||
uint16_t qdcount = ntohs(dns->qdcount);
|
||||
if (qdcount == 0 || qdcount > 16) return;
|
||||
|
||||
uint16_t udp_payload_len = ntohs(udp->len);
|
||||
if (udp_payload_len < sizeof(udp_hdr_t) + sizeof(dns_hdr_t)) return;
|
||||
uint16_t body_len = udp_payload_len - sizeof(udp_hdr_t);
|
||||
uint8_t *body = (uint8_t *)dns;
|
||||
if (mdns_off + body_len > q->len) return; /* stay within first pbuf chunk */
|
||||
|
||||
ip_hdr_t *ip = (ip_hdr_t *)pkt_at(q, sizeof(eth_hdr_t), sizeof(ip_hdr_t));
|
||||
if (!ip) return;
|
||||
|
||||
uint16_t pos = sizeof(dns_hdr_t);
|
||||
uint16_t i;
|
||||
for (i = 0; i < qdcount; i++) {
|
||||
uint16_t qtype = 0, qclass = 0;
|
||||
bool match = mdns_match_hostname(body, body_len, &pos, &qtype, &qclass);
|
||||
if (match && (qtype == 1 /*A*/ || qtype == 0xFF /*ANY*/) && (qclass & 0x7FFF) == 1) {
|
||||
send_mdns_a_reply(client_mac, ip->src, ntohs(udp->src_port), ntohs(dns->id));
|
||||
return;
|
||||
}
|
||||
if (pos == 0 || pos > body_len) return;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Hooks
|
||||
* ------------------------------------------------------------------------- */
|
||||
@@ -368,13 +505,33 @@ static err_t ICACHE_FLASH_ATTR bridge_output_sta(struct netif *netif, struct pbu
|
||||
err_t err = s_orig_lo_ap(s_ap_nif, p);
|
||||
pbuf_header(p, -(s16_t)sizeof(eth_hdr_t)); return err;
|
||||
}
|
||||
|
||||
/* IP multicast (224.0.0.0/4): copy to AP side with the standard
|
||||
01:00:5e multicast MAC before also forwarding upstream. */
|
||||
uint32_t hip = ntohl(ipaddr->addr);
|
||||
if ((hip >> 28) == 0xE && pbuf_header(p, sizeof(eth_hdr_t)) == 0) {
|
||||
eth_hdr_t *eth = (eth_hdr_t *)p->payload;
|
||||
eth->dst[0] = 0x01; eth->dst[1] = 0x00; eth->dst[2] = 0x5e;
|
||||
eth->dst[3] = (uint8_t)((hip >> 16) & 0x7f);
|
||||
eth->dst[4] = (uint8_t)((hip >> 8) & 0xff);
|
||||
eth->dst[5] = (uint8_t)( hip & 0xff);
|
||||
os_memcpy(eth->src, s_ap_nif->hwaddr, 6); eth->type = htons(ETHTYPE_IP);
|
||||
s_orig_lo_ap(s_ap_nif, p);
|
||||
pbuf_header(p, -(s16_t)sizeof(eth_hdr_t));
|
||||
}
|
||||
|
||||
return s_orig_output_sta(netif, p, ipaddr);
|
||||
}
|
||||
|
||||
static err_t ICACHE_FLASH_ATTR bridge_output_ap(struct netif *netif, struct pbuf *p, ip_addr_t *ipaddr)
|
||||
{
|
||||
if (fdb_lookup(ipaddr->addr)) return s_orig_output_ap(netif, p, ipaddr);
|
||||
|
||||
const uint8_t *client_mac = fdb_lookup(ipaddr->addr);
|
||||
if (client_mac && pbuf_header(p, sizeof(eth_hdr_t)) == 0) {
|
||||
eth_hdr_t *eth = (eth_hdr_t *)p->payload;
|
||||
os_memcpy(eth->dst, client_mac, 6); os_memcpy(eth->src, s_ap_nif->hwaddr, 6); eth->type = htons(ETHTYPE_IP);
|
||||
err_t err = s_orig_lo_ap(s_ap_nif, p);
|
||||
pbuf_header(p, -(s16_t)sizeof(eth_hdr_t)); return err;
|
||||
}
|
||||
return s_orig_output_sta(s_sta_nif, p, ipaddr);
|
||||
}
|
||||
|
||||
@@ -398,9 +555,23 @@ static err_t ICACHE_FLASH_ATTR bridge_input_ap(struct pbuf *p, struct netif *inp
|
||||
|
||||
eth_hdr_t *eth = (eth_hdr_t *)q->payload;
|
||||
bool is_bcast = (eth->dst[0] & 0x01) != 0, is_to_ap_mac = (os_memcmp(eth->dst, s_ap_nif->hwaddr, 6) == 0);
|
||||
uint16_t eth_type = ntohs(eth->type);
|
||||
|
||||
/* Learn source IP→MAC before any early return so replies to the management
|
||||
address (which take the is_to_ap_mac path) can be routed back via the FDB. */
|
||||
{
|
||||
const uint8_t *src_mac = ((eth_hdr_t *)p->payload)->src;
|
||||
if (eth_type == ETHTYPE_ARP) {
|
||||
arp_hdr_t *arp = (arp_hdr_t *)pkt_at(q, sizeof(eth_hdr_t), sizeof(arp_hdr_t));
|
||||
if (arp) fdb_insert(arp->spa, src_mac);
|
||||
} else if (eth_type == ETHTYPE_IP) {
|
||||
ip_hdr_t *ip = (ip_hdr_t *)pkt_at(q, sizeof(eth_hdr_t), sizeof(ip_hdr_t));
|
||||
if (ip) fdb_insert(ip->src, src_mac);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_to_ap_mac && !is_bcast) { pbuf_free(q); return s_orig_input_ap(p, inp); }
|
||||
|
||||
uint16_t eth_type = ntohs(eth->type);
|
||||
bool handled = false;
|
||||
os_memcpy(eth->src, s_sta_nif->hwaddr, 6);
|
||||
|
||||
@@ -422,6 +593,8 @@ static err_t ICACHE_FLASH_ATTR bridge_input_ap(struct pbuf *p, struct netif *inp
|
||||
uint16_t udp_off = sizeof(eth_hdr_t) + (ip->vhl & 0x0f) * 4;
|
||||
udp_hdr_t *udp = (udp_hdr_t *)pkt_at(q, udp_off, sizeof(udp_hdr_t));
|
||||
if (udp && ntohs(udp->dst_port) == 67) snoop_dhcp_request(q, udp_off + sizeof(udp_hdr_t));
|
||||
if (udp && ntohs(udp->dst_port) == 5353)
|
||||
handle_ap_mdns_query(q, udp_off, ((eth_hdr_t *)p->payload)->src);
|
||||
}
|
||||
s_orig_lo_sta(s_sta_nif, q); handled = true;
|
||||
}
|
||||
@@ -559,9 +732,6 @@ void ICACHE_FLASH_ATTR bridge_init(struct netif *sta_nif, struct netif *ap_nif)
|
||||
os_memset(s_fdb, 0, sizeof(s_fdb)); os_memset(s_xid_map, 0, sizeof(s_xid_map));
|
||||
struct softap_config ap_cfg; wifi_softap_get_config(&ap_cfg); ap_cfg.channel = my_channel; wifi_softap_set_config(&ap_cfg);
|
||||
wifi_set_sleep_type(NONE_SLEEP_T);
|
||||
os_timer_disarm(&s_arp_timer);
|
||||
os_timer_setfn(&s_arp_timer, (os_timer_func_t *)arp_timer_func, NULL);
|
||||
os_timer_arm(&s_arp_timer, 30000, 1);
|
||||
os_printf("bridge: init done\n");
|
||||
}
|
||||
|
||||
|
||||
@@ -217,6 +217,14 @@
|
||||
#define GPIO_CMDS 1
|
||||
#endif
|
||||
|
||||
//
|
||||
// Define this to 1 to advertise the repeater via mDNS on both STA and AP interfaces.
|
||||
// The device will be reachable as <sta_hostname>.local
|
||||
//
|
||||
#ifndef MDNS_REPEATER
|
||||
#define MDNS_REPEATER 1
|
||||
#endif
|
||||
|
||||
// Internal
|
||||
|
||||
typedef enum {
|
||||
|
||||
+27
-2
@@ -35,6 +35,9 @@
|
||||
#include "ringbuf.h"
|
||||
#include "user_config.h"
|
||||
#include "config_flash.h"
|
||||
#if MDNS_REPEATER
|
||||
#include "lwip/mdns.h"
|
||||
#endif
|
||||
#ifdef REPEATER_MODE
|
||||
#include "bridge.h"
|
||||
#endif
|
||||
@@ -1193,7 +1196,8 @@ void ICACHE_FLASH_ATTR console_handle_command(struct espconn *pespconn)
|
||||
to_console(response);
|
||||
#ifndef REPEATER_MODE
|
||||
os_sprintf_flash(response, "set [network|dns|ip|netmask|gw] <val>\r\n");
|
||||
to_console(response);#endif
|
||||
to_console(response);
|
||||
#endif
|
||||
#if HAVE_ENC28J60
|
||||
#if DCHPSERVER_ENC28J60
|
||||
os_sprintf_flash(response, "set [eth_dhcpd] <val>\r\n");
|
||||
@@ -3816,7 +3820,24 @@ static void ICACHE_FLASH_ATTR user_procTask(os_event_t *events)
|
||||
switch (events->sig)
|
||||
{
|
||||
case SIG_START_SERVER:
|
||||
// Anything else to do here, when the repeater has received its IP?
|
||||
#if MDNS_REPEATER
|
||||
{
|
||||
static struct mdns_info mdns;
|
||||
os_memset(&mdns, 0, sizeof(mdns));
|
||||
mdns.host_name = "esp-wifi-repeater";
|
||||
mdns.server_name = "http";
|
||||
mdns.ipAddr = my_ip.addr;
|
||||
#if WEB_CONFIG
|
||||
mdns.server_port = config.web_port;
|
||||
#else
|
||||
mdns.server_port = 80;
|
||||
#endif
|
||||
wifi_set_broadcast_if(0x03); /* STA(0x01) | AP(0x02) */
|
||||
espconn_mdns_init(&mdns);
|
||||
os_printf("mDNS: started as esp-wifi-repeater.local (_http._tcp port %d)\r\n",
|
||||
mdns.server_port);
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
|
||||
case SIG_CONSOLE_TX:
|
||||
@@ -3915,6 +3936,10 @@ void wifi_handle_event_cb(System_Event_t *evt)
|
||||
os_printf("disconnect from ssid %s, reason %d\r\n", evt->event_info.disconnected.ssid, evt->event_info.disconnected.reason);
|
||||
connected = false;
|
||||
|
||||
#if MDNS_REPEATER
|
||||
espconn_mdns_close();
|
||||
#endif
|
||||
|
||||
#if MQTT_CLIENT
|
||||
if (mqtt_enabled)
|
||||
MQTT_Disconnect(&mqttClient);
|
||||
|
||||
Reference in New Issue
Block a user