Skip to content

blackice 1.0.7

Latest

Choose a tag to compare

@ValentinTorassa ValentinTorassa released this 11 Nov 18:02
Immutable release. Only release title and notes can be modified.

New:

  • blackice deny --all forgets every approval, with one Touch ID. What waits keeps waiting, remembered denials
    stay, and connections already let through go on. Since 1.0.6 approvals outlast a stop, an upgrade and a restart
    of the Mac, and blackice deny takes back one app and host at a time: nothing cleared them all. It works while
    blackice is on; off, it says so before asking for Touch ID, and the saved approvals wait for the next start.
  • An upgrade no longer starts the new filter beside the old one. On macOS 27.0.1 the kernel detaches a stopped
    filter's sockets one by one and can stay at one of them: at a UDP socket whose flow table had two flows and has
    one left, which it takes again and again until the socket closes or gets another flow, and at an ICMP socket
    bound to no address, until a packet reaches it. While it does, the filter stays registered. A filter started
    meanwhile was held with it: every new socket was attached to both, the one going gave no verdict, and no new
    connection got through, lookups included. On 10 and 11 November four of five upgrades were held that way, for
    16.3, 12.6, 4.1 and 25.8 s, the last on a socket of Tailscale's network extension. A filter going with none in
    its place holds nothing up: in a blackice stop held 10 s a lookup a second went on in 0.21 s or less. So when
    macOS runs an older build, start turns the filter off, waits until the kernel has let the old one go, and only
    then replaces the extension and turns the filter on. Connections are not checked meanwhile, and start says so
    once the wait has lasted a second, with the process whose socket macOS waits on: It waits on a UDP socket of mDNSResponder (pid 474): it is done when that socket closes or sends to another address. On 11 November an
    upgrade with a test socket kept open 10 s went that way: the old filter was held 10.2 s, the new one started 0.3
    s after the kernel let the old one go, and a lookup a second took 0.13 s or less throughout; a test socket of the
    same kind had held the upgrade before it for 10.3 s, with lookups waiting up to 9.8 s. While it waits, start
    sends an ICMP echo request to 127.0.0.1, 0.3 s in and every 2 s after. Three upgrades that day were held at an
    ICMP socket of macOS's own symptomsd, for 58.9, 47.3 and 116.0 s, longer than start then waited (30 s twice, 90
    s once): the kernel let go only 28.5, 17.0 and 25.8 s after the new filter had started and stopped every new
    connection. An echo reply goes to every ICMP socket bound to no address: in a blackice stop held 8 s at that
    socket one ping of 127.0.0.1 ended it within 0.01 s, and in the next upgrade, at that socket again, the kernel
    let go 0.4 s after the filter was turned off, the new filter started half a second later, start said nothing of
    it, and a lookup a second took 0.13 s or less. The request does not leave the Mac. stop sends it too, for 3 s
    at most, so that the kernel is not left at it: in a stop at that socket it let go in 0.4 s. A start after a
    stop waits as an upgrade does. A UDP socket is not ended this way: there start waits 90 s at most, then goes
    on anyway, and it and blackice status (a network line) say that no new connection gets through until macOS is
    done. blackice reads the kernel's lists of filters and of the sockets attached to them, which any process may
    read; where they are not laid out as on macOS 27.0.1, start upgrades as before and says nothing.

Fixed:

  • blackice stop says that macOS asks next. After Touch ID, macOS has an administrator confirm removing the
    network extension in a dialog of its own and does not tell the app that asked, so stop waited on that dialog
    having said nothing: 78 s on 11 November. It now says macOS now asks you to confirm removing the network extension: follow its dialog. before it asks macOS, unless macOS answers that it has no extension of blackice's
    enabled.
  • Every connection the filter drops is counted, and delayed once its app has had 64 dropped within 2 s, wherever
    it is dropped. 1.0.6 counted only a connection dropped as it started: one dropped once its TLS hello was read,
    after a lookup, or for a full table was dropped at once. On 10 November, on 1.0.6, node had 7,586 connections
    dropped as denied;remembered in 55 minutes after the agent denied it registry.npmjs.org, about 1,300 of them
    within one second three times, and none was delayed.
  • A host that is an alias of the name its address came under passes the address check. The filter reads the
    system resolver's answers and keeps, for each address, the name the answer was for. The resolver keeps each
    CNAME of a chain for its own TTL and, once the addresses at the end have expired, asks only for the name they
    belong to, so that answer names the end of the chain and not the host the app asked for. The filter then looked
    the host up itself and could get other addresses. On 10 November outlook.office365.com was three CNAMEs of 300,
    300 and 45 s and eight addresses of 15 s, with eight others at each query: a mail CLI's approved connections
    there were dropped as address_mismatch in 10 of 239 runs, 9 of them among the 34 runs in which the resolver
    sent three queries, and each time the CLI got through on a later address. The filter now keeps the CNAMEs it
    reads, each as long as the resolver keeps it, and follows them back from an address's name.
  • The names and CNAMEs the filter reads last as long as the resolver answers from them. mDNSResponder keeps a
    record for its TTL, a quarter more and 2 s, and for at least 15 s: asked for outlook.office365.com twice a
    second on 10 November, it asked the network again 77 s after a CNAME of 60 s, 39 s after one of 30 s and 15 s
    after addresses of 3 to 9 s. The filter kept a name for its TTL, so the name went here while the resolver
    still handed its addresses to apps, and the filter looked the host up itself. With the CNAMEs followed back
    and nothing else, the mail CLI's listing every 10 s still failed on address_mismatch in 2 of 180 runs. In
    the first the resolver followed a CNAME 322 s old whose TTL was 295 s, and gave the CLI the addresses under
    one name and the filter's lookup, 65 ms later, those under another. Names and CNAMEs now last that long and
    5 s more, for a TTL counted as 30 s at least, and at most 5 minutes for a name and a day for a CNAME. With
    both, the listing ran 180 times on 10 November with no connection dropped. On 11 November, with Tailscale's
    resolver, it ran 180 times and none failed, and in one run six connections to login.microsoftonline.com were
    dropped as address_mismatch within 6 ms, 5 ms after the resolver had that host's addresses anew; the run's
    next connection passed. Why the answer the filter read then did not cover the host is not known. An alias is
    dropped this way less often, not never.
  • A burst of DNS lookups no longer leaves the answers after it unread. The filter read at most 256 of the
    resolver's DNS flows at once, counted each for two more minutes once it had let it go, and did not read a new
    flow when the table was full. On 10 November a network came back after an outage with 267 resolver flows in
    two minutes, 156 of them in one second: the table stayed full for two minutes, the filter looked
    outlook.office365.com up itself five times in them, and the second of the two failed runs above was one. A
    test's 611 resolver flows in half a minute did the same to the next host looked up. The filter now reads 512 flows
    at once, a new flow takes the place of the one quiet the longest, and the flows let go are counted apart, at
    most 4,096. A flow let go to make room with a query still unanswered is counted, each minute, in control dns_flows_unread.
  • A restart of the filter no longer cuts a VPN whose client a host rule covers. A filter that has just started
    can be given a UDP socket that was open before it as a new flow, and it has read no DNS answer yet, so it looked
    the flow's host up itself with the flow paused. For a VPN client's own flow that lookup goes into the tunnel the
    paused flow carries, and neither can finish. On 10 November an upgrade did that to the AWS VPN Client, which a
    rule for *.amazonaws.com covers: the lookup of its server's name got no answer for 30 s, the flow was dropped
    as address_mismatch, and with the Mac's DNS server behind the dead tunnel no name resolved for most of the next
    8 minutes. The filter now saves the app, host, address and port of each UDP flow it lets through on a host's
    name, and for two minutes after a start a UDP flow of the same app to the same address and port is decided on
    that name with no lookup. Rules and approvals decide it as always. A pair is used for 30 days after the last
    time a lookup or a DNS answer gave the host that address; a flow that passes on a saved pair does not renew it.
    The first start of 1.0.7 has nothing saved: disconnect such a VPN before upgrading from 1.0.6.
  • A TCP connection to a link-local IPv6 address is asked about. It was dropped as no_host with no event, since
    the address could be any host on any link without its interface: on 10 November identityservicesd's
    connections to a peer on a utun interface all were. macOS gives the filter no interface for a connection's
    destination: one to fe80::dead:beef%en0 arrives as fe80::dead:beef (macOS 27.0.1). So the event names the
    address alone, and an approval covers it on every interface, as an address entry of a rule does. approve --host and deny --host take a link-local address with no interface; until now they took one only with an
    interface, and that matched no connection.
  • npm's folder is no longer its script. node writes npm's title over its own command line (npm run build where
    node, npm's path and its arguments were), so the argument the script was in reads as empty, and the filter
    joined that to the process's folder: the event's app.script was the folder npm ran in, and each folder was
    asked about and answered apart. From 7 to 10 November, 15 of node's 77 events with a script named a folder that
    still exists, and on 10 November the agent denied node registry.npmjs.org nine times, each for a script path
    under /private/tmp. A process that wrote over its command line now has no script, and its event has app.title:
    the first two words of the title it gave itself, and a third after run or exec, where it names a script or a
    package; none from the first option, assignment or URL on, cut to 64 bytes. A title is a command line again, and
    can hold a token as arguments can: npm 11.20.0 titles npm run x -- --token … as npm run x --token … and npm start -- sk-… as npm start sk-…, and the events show npm run x and npm start. The agent reads it,
    blackice pending and the menu show it (calls itself npm run build), events.jsonl keeps it as app_title,
    and no decision reads it:
    the process chose it. The agent's answer for such an event covers the interpreter whenever no script can be read,
    whatever its title; a person's approve with no --script covers every script the interpreter runs, as before.
  • An environment string is never read as a script. macOS gives a process's arguments and then its environment
    with nothing between them, and a title as long as the arguments leaves no NUL to count them by: on 10 November
    node x.js with a title of 20 bytes read as my-long-s and then its first environment string, which 1.0.6
    would have shown as the script, value and all. None of the 182 events with a script in this Mac's log holds
    one. An argument with = before any / is now no script.

Upgrading from 1.0.6: brew upgrade --cask blackice, or the install script with --force, then blackice start.
start turns the filter off while macOS disconnects the old build's filter, and says so if that lasts over a
second. Disconnect a VPN whose client a host rule covers before this upgrade: the first start of 1.0.7 has no saved
destinations yet.

Blackice.app 1.0.7, build 58, built from 44f4148, signed with the Developer ID of team
M294LK997M, notarized and stapled. The tag is three commits later: they add this zip's SHA-256 to the install script,
the record of the test (evidence/P5-22), what the day's upgrades showed to the changelog, the plan and the install
notes, and a correction to a comment.
Blackice-1.0.7.zip SHA-256: 1e013215d2dd45b7acd25797d281f4eb5d69dc085ea0d4f56213005ea097e018