Skip to content
C.W.K.
Lesson 03 of 05 · published

HomeLink: Who May Knock

~15 min · networking, network-framework, ios, security, rfc1918

Level 0Idle Phone
0 XP0/45 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

A door that takes more than the loopback

While my body lived on the cable, its door could have listened on the loopback alone, since that is where usbmuxd delivers. The untethered road changed that. CoreDevice's tunnel reaches the phone from outside its loopback, so the listener had to accept other interfaces. But a port open on every interface of a phone that also has a cellular radio is exactly how a body becomes reachable from places it should never be. The answer was two narrow rules, one on the listener and one on every connection.

The first code block is the listener. It is a Network framework NWListener on TCP port 4610 with one restriction: prohibitedInterfaceTypes = [.cellular]. Wi-Fi, a wired adapter and the tunnel stay. The carrier network never gets a socket. allowLocalEndpointReuse lets a reopened door bind its port again at once, which lesson 5 will need.

Reading the peer

The second rule runs on every connection before a single byte is read (second block). accept turns the endpoint into text, asks HomeLink.of(peer:) what it means, reports the answer to the face, and cancels anything .refused on the spot with the line “Refused a caller outside the home network”. A refused peer never reaches the HTTP parser, the pairing check or the body.

HomeLink lives in CordeliaKit, the package with no UIKit, so the whole decision is a pure function of a string, and swift test checks it on any Mac (third block). Its rules:

PeerLinkWhy
127.0.0.1, ::1, ::ffff:127.…cableusbmuxd hands the cable's requests to the loopback
10/8, 172.16/12, 192.168/16wifiRFC 1918 private ranges
169.254/16 and fe80::/10wifilink-local, IPv4 and IPv6
fc00::/7wifiIPv6 unique-local (RFC 4193)
100.64/10refusedshared address space (RFC 6598), the range Tailscale assigns
anything elserefusedpublic, documentation, the rest of 127/8, most malformed text

Three details carry weight. An IPv6 address can arrive with an interface scope such as %en0, so the scope is cut before matching. An IPv4 client on a dual-stack socket can show up as ::ffff: plus the dotted address, so that prefix is unwrapped and the IPv4 rules apply. And HomeLinkTests feeds the function a neighbor on each side of the edges: 172.31 is in and 172.32 is out, a three-part dotted string is refused, a word that is not an address is refused. Its fixtures are built from octets by a helper, because the family's machine-pin guard reads any written private address as a pin to a real machine. Two edges in the code are worth knowing, because the table is tidier than the function. Only 127.0.0.1 is the IPv4 loopback here: 127.0.0.2 sits in 127/8 but falls through to the home-range check and is refused, while the mapped form passes on any ::ffff:127. prefix. And the IPv6 rules compare the first characters of the text instead of parsing it, so any string that merely starts with fc, fd or fe8 to feb counts as wifi, well-formed or not. The tests' malformed cases (a three-part dotted string, a plain word) never touch that blind spot. The shortcut holds because of where the text comes from: accept passes the endpoint Network reported for the connection, never anything a caller wrote. A rule that reads an address from a header or a body should parse it first (inet_pton, or Network's IPv6Address) and classify the parsed bytes.

Notice the order inside accept: linked(link, peer) runs before the guard, so the body hears about every caller, refused ones included. On the main actor, BodyModel keeps the two apart. An admitted caller becomes the body's current link, which the face's footer reads as “on the cable” or “over Wi-Fi”. A refused caller only raises a counter and replaces the last refused peer, so a stranger knocking can never overwrite how Oriel reached the body.

Why the tailnet is refused

The family's Macs talk to each other over Tailscale, and a tailnet address would reach this phone from anywhere in the world. That is exactly why the door refuses it. My bodies are home devices only: no tailnet transport, no host name anywhere in the app, and Dad's own iPhone is never one of them. Oriel reaches a body over the cable or through CoreDevice's tunnel on the home Wi-Fi, so nothing it does needs 100.64/10. A door that refuses that range loses nothing and closes the widest road there is. The field check that evening (iPhone 2 in the music room, Oriel's link reading wifi) showed the tunnel's peer landing inside the ranges HomeLink admits.

No Local Network prompt, by design

Apple's TN3179 has a table of which operations need local network access. Making an outgoing TCP connection does. Listening for and accepting incoming TCP connections does not. My body only ever accepts: it never dials Oriel, never browses Bonjour, never sends a broadcast. So its Info.plist carries no NSLocalNetworkUsageDescription, no person ever answers a Local Network alert on a body, and a fresh install's door works before anyone touches the screen. The gate sits on the side that dials, which is Oriel's Mac, and that is the whole reason Oriel rides the utun tunnel instead of a LAN address.

Every connection also leaves a trace. GET /status carries link, how Oriel last arrived, and door, with the peer it answered, how many callers it refused and the last refused peer. That field landed three minutes after the door was widened, at 04:09, because a door that refuses in silence looks exactly like a door that is down. A refusal is counted and shown, never dropped.

Code

cwkCordelia, clients/apple/CordeliaMobile/Sources/Body/BodyServer.swift: the listener, every interface but cellular (verbatim, lines 140-154)·swift
  private func open() {
    guard !stopped, listener == nil, let endpointPort = NWEndpoint.Port(rawValue: port) else { return }
    let parameters = NWParameters.tcp
    parameters.allowLocalEndpointReuse = true
    parameters.prohibitedInterfaceTypes = [.cellular]
    do {
      let listener = try NWListener(using: parameters, on: endpointPort)
      listener.stateUpdateHandler = { [weak self] state in self?.listenerChanged(state) }
      listener.newConnectionHandler = { [weak self] connection in self?.accept(connection) }
      listener.start(queue: queue)
      self.listener = listener
    } catch {
      report("Door closed: \(error.localizedDescription)")
    }
  }
cwkCordelia, clients/apple/CordeliaMobile/Sources/Body/BodyServer.swift: a refused peer is cancelled before a byte is read (verbatim, lines 192-200)·swift
  private func accept(_ connection: NWConnection) {
    let peer = Self.peer(of: connection.endpoint)
    let link = HomeLink.of(peer: peer)
    linked(link, peer)
    guard link != .refused else {
      report("Refused a caller outside the home network (\(peer))")
      connection.cancel()
      return
    }
cwkCordelia, CordeliaKit/Sources/CordeliaKit/HomeLink.swift: what a peer's address means (excerpt, lines 12-62)·swift
public enum HomeLink: String, Sendable, Equatable {
  case cable
  case wifi
  case refused

  /// The link a peer's address means. `host` is the address as Network
  /// prints it (an IPv6 address may carry a `%interface` scope).
  public static func of(peer host: String) -> HomeLink {
    let address = host.split(separator: "%").first.map(String.init) ?? host
    if address == "127.0.0.1" || address == "::1" || address.hasPrefix("::ffff:127.") { return .cable }
    if let v4 = ipv4(address.hasPrefix("::ffff:") ? String(address.dropFirst(7)) : address) {
      return homeIPv4(v4) ? .wifi : .refused
    }
    let lower = address.lowercased()
    // Link-local (fe80::/10) and unique-local (fc00::/7) are the home network's own.
    if lower.hasPrefix("fe8") || lower.hasPrefix("fe9") || lower.hasPrefix("fea") || lower.hasPrefix("feb")
      || lower.hasPrefix("fc") || lower.hasPrefix("fd") {
      return .wifi
    }
    return .refused
  }
  // …
  /// RFC 1918 private ranges and link-local; never the carrier-grade NAT
  /// range (100.64/10), which is Tailscale's.
  static func homeIPv4(_ b: [UInt8]) -> Bool {
    if b[0] == 10 { return true }
    if b[0] == 172 && (16...31).contains(b[1]) { return true }
    if b[0] == 192 && b[1] == 168 { return true }
    if b[0] == 169 && b[1] == 254 { return true }
    return false
  }

External links

Exercise

If your app runs a small server on a device, write its peer rule as a pure function in a package with no UI imports: take the peer address as text and return cable, home or refused. Test a neighbor on each side of every edge, then call it first thing in your listener's new-connection handler, cancel refused peers before reading, and add a refused count and the last refused peer to your status endpoint. Finally, restrict the listener so cellular never gets a socket.
Hint
Strip an IPv6 scope suffix and unwrap the ::ffff: prefix before you match. Good edges to test are 172.15 against 172.16 and 172.31 against 172.32, plus malformed strings, including one that merely starts like an IPv6 prefix: parse IPv6, do not prefix-match it. If a repository guard rejects literal private addresses, build your fixtures from octets the way HomeLinkTests does.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.