Skip to the main content
mkfifo.com

Holds, mirrors, privacy and limits

Updated planned discovered changed fixed

The name of a FIFO is its key. This post tells how a FIFO holds a different FIFO, how two FIFOs share one envelope, what mkfifo.com keeps private, and which limits came before the launch.

Holding another FIFO

A public form at /hello should not show its rules to each person who guesses the name. So a FIFO can hold a different, free FIFO, and the held FIFO can have a page.

  • The ingress envelopes have To: hello, but they live in the editor of a secret name, for example hello-k7f2.
  • /hello has no editor. PUT and DELETE get 409, and GET /hello is a Request, or the page below.
  • The To of an ingress envelope does not change after the save.

Locks, again

The model (specs/Hold.tla) showed two wrong locks:

  1. With the lock of the home only, two FIFOs held one name.
  2. With the lock of the held name only, two FIFOs held the names of each other. Their envelopes had no editor at all.

So: one lock for all the saves of ingress envelopes. A property test runs the actions of the model against the real code, with processes at the same time.

A page for a held FIFO

A held FIFO can have a page: its public envelopes, and a letter in the shape of each one, which each person can send.

  • An ingress envelope is public when it has a note. The others stay private, because a pattern can hold a secret.
  • mkfifo.com draws all of the page, so it cannot look like a different site. Each page has a banner that the holder cannot remove.
  • The page does not show the holder, the destinations, the sealed patterns, or the Requests of other people.
  • While you type, each field shows if its rule agrees. Nothing goes until you send.

A GET stays a GET

GET /hello is a Request with the method GET. But the method of a Request came first from the header field X-HTTP-Method-Override of the client, then from ?_method=, and only then from HTTP. So GET /hello?_method=POST was a POST for the envelopes. A link, or an <img> on a different site, could send a letter to an envelope for POST, from the browser of a person who did not know it. Now a GET is always GET. Only a POST can change its method.

A link with fields

A GET had no fields, so a short link could go to one place only. Now the query of a GET is a form, as a <form method="get"> sends it. GET /go?to=docs has the field to, and the envelope reads it with Content-Type: form and a field line:

To: go
X-HTTP-Method-Override: ^GET$
Content-Type: form

to: ^(?<to>[a-z]+)$

This made an old problem larger. An envelope with no line for the method agreed with each Request, also with a GET. With fields in the query, a link on a different site could fill a public form, from the browser of a person who only opened a page. So a GET now agrees only with an envelope that has a line X-HTTP-Method-Override. An envelope asks for a GET; the others get only a POST. When a GET does not agree for this reason, the list "Requests" of the editor tells it.

Privacy

Two promises: mkfifo.com keeps no copy of the Requests that go through it, and a secret in an envelope stays sealed.

No copy of a Request

I wanted this as a promise that a test can check.

  • Not in the database, not in the process of a FIFO, and not in the log at the level info and higher.
  • A property test sends a random secret through a FIFO, then looks for it in the log, in the database and in the process.
  • The invocation logs of Cloudflare are off.
  • The editor that is open shows each Request live, with the result of each envelope. It keeps no copy after it shows it.

The home page tells it, and the privacy page has the details.

The cookie of the session

A Request has the header fields of the HTTP request, also Cookie. When a browser sent a form to a FIFO, or opened the link of a held FIFO, its Cookie had the session of mkfifo.com. The report hid it, but a capture on Cookie could send it on to a different place. The session held only the token against false forms, so the harm was small, but a session with more in it would go to the owner of a FIFO. Now each Request loses the cookies of mkfifo.com (their names start with _mkfifo), and keeps the other cookies. Later the same day the rule became stricter: a Request keeps only the signed cookies of its own FIFO (see One-time codes and cookies).

Sealed fields

The name of a FIFO is its key (see "The name is the key" below): each person who knows it can read and change its envelopes. A token or a URL with a secret needs more than that.

  • Write <seal:VALUE> as a value, or close the lock of the field in the editor.
  • mkfifo.com encrypts the value for this FIFO (AES-256-GCM, bound to its name), and shows only <sealed:TOKEN>. A copy in a different FIFO does not open.
  • The report of a Request hides a sealed field and its captures.

mkfifo.com does not guess which fields have a secret. You select them.

The name is the key

Whoever knows the name of a FIFO can change it. So a name must be hard to guess, it must not leak, and some names belong to the site.

Two leaks

  1. Each egress email came from NAME@mkfifo.com. So each person who got an email learned the key. Now each email comes from the reply address of its envelope, reply+UUID@mkfifo.com. A reply to it comes back to the FIFO as a Request, with X-MkFIFO-Reply. One day later, the reply address went away: mkfifo.com sends no email for your FIFO now (see Your email service sends your email).
  2. A Request from one FIFO to the next had the name of the first FIFO in From and in Received. So the next FIFO learned the key of the first. Now it has the reply address there too. Later, only Received: by mkfifo.com; DATE.

Random names

A new FIFO gets a random name of three words. Two words gave about 4,000 names. Too few. Three words give about 500,000. When they get used up, the names get a number.

The home page showed the random name as the placeholder of the field: gray text that looks like an example, and goes away when you type. Now the name is the value of the field, and a button with two arrows gives a new one. The button sends a small form of its own, GET /. So it works with no JavaScript, and Enter in the field still makes the FIFO: Enter sends the first button of the form of the field, and the arrows are not in that form.

. and ..

A browser cannot open /. or /..: it goes to /. These two are not valid names now.

Names of the site

When I added holds, I said "no reserved names". Then the site needed /contact, /support and /docs, and the usual mail addresses, such as postmaster. I was wrong. Now priv/repo/seeds/names.txt keeps the names of the site: nobody can save an envelope there, and no FIFO can hold one. blog joined the list later.

Names of two words

A short name is easy to find, and one person can take many short names. So a new FIFO now needs a name of two words or more, with a - between them, as the name of a custom element of HTML: my-shop, or a random name such as brave-red-fox. A new held FIFO, and a new FIFO with a mirror in it, need 6 characters or more too. PUT /shop gets 422, with an example of a name that agrees, and the home page and the editor tell the rule before the save.

  • The rule is only for new names. A FIFO with envelopes keeps its name, also a name of one word, with its holds and its mirrors. A FIFO is set and forgotten, so a FIFO must not stop because a rule changed.
  • "New" took some thought. A FIFO is new when no ingress envelope and no note has its name as the home. For a held name or a mirror, no To, Cc or Bcc of an ingress envelope uses it either. An egress envelope that sends to a name does not count: else each To of an egress could keep a short name open for a new FIFO.
  • The first version examined the name before the rules of the mirrors. So Cc: support got the error of a short name, not "The FIFO support belongs to mkfifo.com". Now the name comes last.
  • The seeds of the site keep their names of one word, as contact and blog. A name of one word, with 4 characters or more, is for a later step: a subscriber who locks a FIFO.
  • A hold is not a lock: each person who knows the name of the holder can change the held FIFO. Give the holder a random name of 26 characters or more (130 bits).
  • Most of the tests used names of one word. A script changed them to my-NAME, only in the strings of the tests. It also changed some data that was not a name, as the subject x, the host of https://hooks.example/ and the type text/html. So I read each change before the tests ran.

Contact and support

/contact changed three times before it stayed.

  1. An HTML form in the seeds.
  2. The page of a held FIFO.
  3. The locked editor of the FIFO contact, with "Try".

I wanted each visitor to see the rules of the form, and to try them. The locked editor does both, with no special template.

Try

"Try" first came on each ingress envelope. One letter for the whole FIFO is better: one form tests all the rules, and the marks come from the envelope that is nearest. ?form opens it at once, so the links Contact and Support open the form. Its button said "Send the letter" first; now it says "Submit", the usual word of a form, for each letter of mkfifo.com.

"Try" is a link with no JavaScript, and a button that opens a dialog with LiveView. With no JavaScript, and on each page before LiveView connects, the page showed two "Try". The button had the class hidden, and also inline-flex from the class of all the buttons. Both set display, and in the CSS .inline-flex comes after .hidden, so the button always showed. Now the link and the button each set only their own display, and a test examines their classes.

No secret in the Worker

The address that gets the messages went from two secrets of the Worker to a sealed value in the seed. The Worker needs no secret for the site now.

Starting over

The seed saves only in an empty FIFO, so a new seed needs a new database. Cloudflare cannot delete the storage of one Durable Object. A new name of the object gives the site a new database: the old object keeps its data, but nothing reads it.

Later, support started to share the envelope of contact (see "Mirrors" below).

Mirrors

contact and support had two copies of one envelope. Now they share one, with Cc and Bcc, as in an email. It took me three tries to get there.

The path

  • A custom field. My first idea was a "mirror" with a header field of my own. It looked wrong next to the other fields.
  • Cc and Bcc. An email already has them. An ingress envelope with Cc: support also gets the Requests to support, and the editor of support shows it as a mirror. A FIFO can have the mirrors of many envelopes.
  • Which FIFO got it? I thought of a variable {{fifo.name}}, then of Referer. But the client writes Referer: a form on a different site sends that site, and an email has none. So each Request now has Delivered-To (RFC 9228). mkfifo.com sets it and removes the value of the client. Delivered-To: ^(?<fifo>[a-z0-9._-]+)@ keeps the name.
  • A wrong idea. For a while I thought that a variable can come from a header field. It cannot: a variable comes only from a capture.
  • Storage. I planned a new table. Not necessary: a mirror is one row of the table messages, with no text.

Safety

A mirror gets each Request to its FIFO. So nobody can put a mirror in a name of the site, or in a FIFO that a different FIFO holds, and the editor of a FIFO can remove each of its mirrors. A mirror does not show the name of its home, its Bcc lines, or its destinations: the name is the key.

Found before the merge

  • The check "a different FIFO holds this FIFO" also counted the own envelopes of that FIFO. So nobody could put a mirror in a FIFO with envelopes. A test found it.
  • specs/Hold.tla got mirrors: without one lock for all the saves, TLC finds a mirror in a held FIFO.

Why not Cc in an egress envelope

An egress envelope keeps one destination. The stop link, the confirmation, the result, the privacy of the addresses, the seal and the loops each work for one destination. Two egress envelopes for one ingress envelope already send to two places, and the new button "Duplicate" makes the second one with one click.

Limits before the launch

Anybody can make a FIFO, send a letter and save an envelope, with no account. That is the point of mkfifo.com. It is also what a spammer wants. So before the launch, I added limits: on the names, and on the number of actions.

Names that look alike

Some names belong to the site: contact, docs, noreply. But supp0rt, contact-us and mkfifo-billing were free, and a page there could look like a page of mkfifo.com.

Now a name of the site also keeps each name that looks like it. I took the idea from the skeletons of Unicode (UTS #39): two names with one skeleton look alike. My skeleton is in lowercase, drops -, _ and ., and puts one letter for the characters that look alike: 0 is o, 1 and i are l, rn is m. A name with mkfifo in its skeleton is kept too.

This costs some good names. mall looks like mail in some fonts, so nobody can take mall. I prefer a few lost names to one false page.

Addresses for reports

abuse, postmaster and security (RFC 2142) were kept names, but an email to them was rejected. That is bad for a site that asks for reports. Now they are mirrors of contact, and /.well-known/security.txt (RFC 9116) gives security@mkfifo.com.

Rate limits

Each client address now has limits in a window of time:

ActionLimit
Requests to one FIFO120 in 1 minute
Requests and letters to the FIFOs of the site20 in 10 minutes
Letters of a page or of "Try"60 in 10 minutes
Saves and deletes60 in 10 minutes
Copies of a template10 in 1 hour

Over a limit, the answer is 429 with Retry-After, and the editor tells how long to wait.

Only Cloudflare knows the address of a client. The Worker gives it to the VM in a header field, and removes the value that a client puts in that field. The VM counts an HMAC of the address, not the address. An IPv6 address counts with its /64 network, because one client often has all of it.

A form of the editor is also a POST /NAME. So a Request counts after the parse of its body, and a save counts as a save. An email does not count, because Cloudflare Email Routing limits the emails. Only the Worker can mark a request as an email, so a client cannot use the mark to skip the limit.

In front of all this, the Worker has the rate limit of Cloudflare: 600 requests in one minute from one address. A flood stops there, before the Durable Object.

A limit only on paper

The specification said: at most 16 ingress envelopes in a FIFO, and 4 egress envelopes for one ingress envelope. The save did not examine it. I saw it when I tried the rate limit in Wrangler: 60 saves into one FIFO, and all 60 got 201. Now the save refuses the seventeenth. The saves of a FIFO go through its process one at a time, so two saves cannot both take the last place. A test sends 20 saves at the same time, and exactly 16 succeed.