The honk dialect of dae configuration
honk reads a dialect of dae configuration syntax. Where the same text has different meanings in honk and dae, honk's documented behavior is the contract. The dae column covers only its grammar (dae_config.g4), not value conversion.
Lexical rules
| Input | dae grammar | honk | Cases |
|---|---|---|---|
# inside a bare value, log_file: /tmp/a#b, use_host: /tmp/a#b, or group(hk#suffix) | # is a safe character inside a bare literal | Glued # is data, with legacy-glued-hash at the formerly truncated boundary. Scalar paths retain /tmp/a#b; the subgroup name is hk#suffix. Insert whitespace before an intended comment. | dialect-lexical-01-scalar-hash, dialect-lexical-01-host-hash, dialect-lexical-01-group-hash |
# glued to an outbound name in a routing rule, domain(x) -> proxy#c | one bare literal, proxy#c | Literal target proxy#c, with legacy-glued-hash; normal target validation still applies. Write -> proxy # comment for a comment. | dialect-lexical-02-outbound-hash |
/* … */ | block comment, skipped | No block-comment syntax. A statement beginning with /* receives unsupported-comment and is ignored; following physical lines still participate in configuration. Use # on each intended comment line. | dialect-lexical-03-block-comment |
[key: value] after a declaration, filter: name(x) [add_latency: -500ms] | accepted as an annotation | Not recognised; the filter is reported as unparseable and ignored. honk has no per-node latency bias. | dialect-lexical-04-annotation |
An apostrophe inside a bare scalar, log_file: /tmp/don't, or an opened traffic argument quote | lexer error for the bare apostrophe | /tmp/don't is literal. Ordinary quotes open only at token head or immediately after unquoted (, , or : (so key:'value' is the same entry as key: 'value'); root include additionally recognizes adjacent quoted paths. A traffic quote that does not close on its physical line fails with located unterminated-quote; close the quote rather than relying on brace recovery. | dialect-lexical-05-apostrophe, dialect-lexical-05-unterminated, dialect-lexical-05-quote-after-colon |
Braces inside quotes, secret: 'a}b', or comments | data | Quoted braces and token-head-comment braces are data. Compact {} inside matcher arguments or a User-Agent suffix remains data; separate unquoted { and } remain structural. Entry-tail compatibility after a glued # does not hide separated structural braces. Changed legacy comment-brace interpretation emits legacy-comment-brace; put the real closing brace outside the comment. | dialect-lexical-06-quoted-brace, dialect-lexical-06-comment-brace, dialect-lexical-06-compact-argument, dialect-lexical-06-ua-brace |
Block opener without a space, global{ … } | whitespace is skipped, so this is a block | block-delimiter-spacing error, whether inline or line-final. Separate block braces from the header with whitespace. Compact empty blocks such as global {} remain accepted; adjacent declarations such as group { a {} b {} } remain separate blocks. | dialect-lexical-07-glued-opener, dialect-lexical-07-compact-empty, dialect-lexical-07-adjacent-groups |
An extra }, or an empty file | an extra } is rejected; empty or comment-only input is accepted | Extra closers are ignored with unmatched-close; an entry or standalone dae document without a block is rejected. Empty or comment-only included .dae fragments contribute no sections. Extra-close compatibility will be reviewed after the first PR2 release. | dialect-lexical-08-extra-closer, dialect-lexical-08-empty, dialect-lexical-08-comment-only |
Group names, 123 {, 香港 {, Hong Kong { | a block name is one ID | The raw header before separated { is the dynamic group name. Unknown top-level names are diagnosed and skipped, not interpreted as plugins. | dialect-lexical-09-numeric, dialect-lexical-09-unicode, dialect-lexical-09-spaces |
Two declarations on one line, global { log_level: debug log_file: x } | whitespace-separated declarations | One declaration: log_level is debug log_file: x. One key: value per line; one-line blocks hold one statement. | dialect-lexical-10-same-line |
The opening brace on the next line, global newline { | whitespace, newlines included, is skipped | Ordinary split headers fail. Only top-level include retains a split opener, including intervening blank/comment lines, with located legacy-include-opener. Put { on the include header line; compatibility will be reviewed after the first PR2 release. | dialect-lexical-11-split-root, dialect-lexical-11-split-include |
Scalars and lists
| Input | dae grammar | honk | Cases |
|---|---|---|---|
A bare value with colons, bind: 127.0.0.1:53 | not one literal; must be quoted | Accepted bare: the value is everything after the first :. | dialect-scalars-01-bare-colons |
A call-shaped value, client_subnet: auto(9.9.9.9) | function expression | The text auto(9.9.9.9) is the value; the setting parses it itself. | dialect-scalars-02-call-value |
Quoted list items, lan_interface: 'eth0', 'eth1' | two literals | Split the list first, then remove one enclosing quote pair per item: eth0, eth1. Changed legacy quoting emits legacy-list-quoting; native_api lists, which had no legacy reading, never do. Whole-quoted comma aggregates remain compatible for check-target lists, with legacy-quoted-list; prefer individually quoted or bare items. | dialect-scalars-03-list, dialect-scalars-03-aggregate |
Booleans t, y, f, n | bare literals | Lenient settings (case-insensitive): true, yes, 1, on are true; false, f, no, n, 0, off, t, y are false. The four single-letter shorthands emit legacy-bool-shorthand; unknown spellings are false with a diagnostic. Prefer true or false. global.nfqueue_enable and legacy experimental.udp_nfqueue.enabled remain strict and reject shorthands. | dialect-scalars-04-t, dialect-scalars-04-y, dialect-scalars-04-f, dialect-scalars-04-n, dialect-scalars-04-strict |
Marks, so_mark_from_dae: 0x10 and so_mark_from_dae: 10 | bare literals | Both are read as hexadecimal: 16 and 16. An unparseable mark falls back to 0 with a diagnostic. | dialect-scalars-05-0x10, dialect-scalars-05-10, dialect-scalars-05-zz |
Fractional seconds in a millisecond setting, check_tolerance: 1.5s | bare literal | 1500 ms. Unparseable millisecond durations (check_tolerance, sniffing_timeout) fall back to the setting's default with a diagnostic; unparseable second durations (check_interval, subscription interval) fall back to 0 with a diagnostic. | dialect-scalars-06-1-5s, dialect-scalars-06-never, dialect-scalars-06-seconds |
fixed_domain_ttl entries x: 60#note, y: '60', z: 60 ignored | 60#note and '60' are literals, 60 ignored is two tokens | Exactly one decimal scalar is required. y stores 60 with legacy-ttl-quoting; x is omitted with invalid-ttl, and z with trailing-value. Use 60 or quoted 60; put explanations after #. | dialect-scalars-07-hash, dialect-scalars-07-quoted, dialect-scalars-07-trailing |
An unknown policy, policy: mystery(...) | any function expression | Falls back to selector with a diagnostic; the argument text is never echoed. | dialect-scalars-08-unknown-policy |
Node and subscription entries
| Input | dae grammar | honk | Cases |
|---|---|---|---|
| A tag on a share link or subscription | ID ':' literal; a bare literal cannot contain :, so links are quoted | For a bare head, the text before the first : is the tag unless that colon starts ://; for a quoted head, only a : right after the closing quote declares a tag, so a colon in an unquoted User-Agent or comment suffix is suffix data. Links may be bare; tags and links may be quoted. In subscription only, a wholly quoted value with a tag inside ('paid:https://…') is split after unquoting, and an entry without a tag is named after its URL host; in node a quoted value is the whole link ('paid:socks5://…' is an unknown scheme). | dialect-entries-01-tagged, dialect-entries-01-bare, dialect-entries-01-quoted-tag, dialect-entries-01-embedded-subtag, dialect-entries-01-embedded-nodetag, dialect-entries-01-tagless |
| A trailing comment on an entry line | comment when # starts a token | A # after a space or tab ends the line. Inside a bare value, glued # is data (…#hk1, ?filter='hk'#token, /tmp/a#b). After a closing node/link quote or one balanced subscription (UA) suffix, a glued # truncates the entry tail with legacy-glued-hash, but is not a lexical comment: a separated brace after it still changes block structure. Put whitespace before an intended comment. Other trailing text skips the entry with trailing-entry-text or legacy-ua-boundary. | dialect-entries-02-comment, dialect-entries-02-glued, dialect-entries-02-trailing |
'url'(User-Agent) after a subscription link | not in the grammar | honk extension: the parenthesised text is the User-Agent; a # after a space inside unquoted parentheses ends the line, so quote such a User-Agent. | dialect-entries-03-user-agent, dialect-entries-03-quoted-agent, dialect-entries-03-unquoted-hash |
A subscription block, paid: { followed by url: …, ua: …, interval: … on their own lines and } | ID ':' '{' is not a declaration | honk extension: the block form of a subscription; its settings are one per line like any block. | dialect-entries-04-block |
A block after a quoted subscription link, paid: 'url' { followed by ua: …, interval: …, cache: … or route: … on their own lines and } | ID ':' literal ends the declaration | honk extension: per-entry options; omitted ones come from assets.subscription and assets.route. route replaces download_detour of the older block form, and an entry that sets both fails with conflicting-subscription-route. | |
A bare tag with a space before the colon in node, edge : 'socks5://…' | tag edge | Whitespace around the colon is normalized: the node is retained with tag edge and an entry-tag-normalized informational diagnostic. edge: … and quoted tags remain accepted. | dialect-entries-05-spaced-tag |
vless_mode in a VLESS share link or flattened serialized VLESS node | legacy VLESS mode selector | Removed, not an accepted alias. Share links must use independent udp=, packetEncoding=, and mux= queries. A flattened VLESS node rejects any vless_mode presence, including null, and uses network, packet_encoding, and multiplex instead. | |
vless_mode: legacy on a flattened serialized non-VLESS node | legacy VLESS field outside VLESS | Compatibility placeholder emitted and consumed by the wire adapter only; it does not select behavior. It is the sole non-VLESS serialization exception, not documented dae or share-link syntax. |
Routing
| Input | dae grammar | honk | Cases |
|---|---|---|---|
Bare prefix matchers, !geosite:cn -> proxy, domain:example.com -> proxy | not function calls; rejected | Accepted as matchers (geosite, geoip, domain, suffix, keyword, regex, full prefixes). | dialect-routing-01-geosite-prefix, dialect-routing-01-domain-prefix |
Two arrows, domain(x) -> proxy->backup | rejected | The outbound is literal proxy->backup, with legacy-arrow-target. Retained for compatibility; normal target validation applies. | dialect-routing-02-two-arrows |
-> proxy( must ) | call with parameter must | Non-direct targets still require the exact suffix (must); proxy( must ) is an outbound named proxy( must ). Direct targets parse their option list, so direct( must ) is terminal. | dialect-routing-03-spaced-must, direct option tests |
Whitespace before the parentheses, dport (443) -> proxy | accepted | Located unknown-traffic-predicate error. Remove whitespace before matcher parentheses. | dialect-routing-04-spaced-call |
An unknown matcher in a conjunction, dport(443) && domian(x) -> direct | accepted by the grammar; validation is outside it | Located unknown-traffic-predicate error, also for negated terms. Correct the matcher; no term is silently discarded. | dialect-routing-05-unknown, dialect-routing-05-negated |
Arguments with several colons, dip(2001:db8::/32), mac(00:11:22:33:44:55) | not literals; quote them | Kept whole: only a recognised prefix: (geosite:, geoip:, domain:, suffix:, keyword:, regex:, full:) is split at its colon. | dialect-routing-06-ipv6, dialect-routing-06-mac |
direct(mark: 10) / direct(mark: 0x10) / direct(mark: 010) | function parameter; dae's typed decoder uses Go base-0 ParseUint, yielding 10 / 16 / 8 | Decimal or explicit 0x/0X hexadecimal only, yielding 10 / 16 / 10. Binary/octal prefixes, underscores, signs, overflow and reserved bits 0xc0000000 reject. This intentionally differs from both dae base-0 decoding and honk's hexadecimal-first global mark scalar above. | direct mark tests |
direct(mark: 512, must) / direct(must, mark: 512) and marked fallback: | option order does not matter | Both orders are accepted for direct rules and fallback, including whitespace. Unknown/duplicate direct options reject safely; only direct targets support marks. The final merged fallback's whole action, including mark and must, applies after every ordinary rule. | parser tests, include tests |
The upstream typed mark and option behavior is defined by routing.ParseOutbound. Runtime low-30-bit policy masks, global defaults and restart requirements are in routing marks and socket marks.
DNS
| Input | dae grammar | honk | Cases |
|---|---|---|---|
Unquoted // in a request or response rule | not a comment | // is data, never a comment. Trailing slash-comment text such as -> asis // note omits the malformed rule with legacy-slash-comment; quoted slashes remain literal. Replace DNS trailing // comments with #. | dialect-dns-01-slash-comment, dialect-dns-01-quoted-slash |
# in a request or response rule, qname(a#b) -> reject # note | a#b is a literal, # note a comment | Only an unquoted token-head # starts a comment, after a space or tab. The pattern remains a#b and the action is reject; changed old reading emits legacy-dns-hash. Keep literal glued hashes; separate comments with whitespace outside quotes. | dialect-dns-02-hash |
-> in a request or response rule | one arrow | The rule splits at the first unquoted ->; further arrows stay in the action with legacy-arrow-target. The whole action is lowercased and must match a declared upstream exactly: MixedCase selects mixedcase, not a declaration named MixedCase. This action/reference namespace is unchanged. | dialect-dns-03-two-arrows, dialect-dns-03-mixed-action |
A matcher call split across lines, qname( newline a.example) -> reject | whitespace, newlines included, is skipped | DNS never assembles lines. Each incomplete nonblank line is omitted with located incomplete-dns-rule, so this example produces two warnings. Put the DNS call and action on one line. An unterminated quote has one lexical diagnostic, not an additional incomplete-line warning; recovery requires every open block to close outside quoted data. | dialect-dns-04-split-call |
An upstream with an outbound, u: 'udp://1.1.1.1:53' -> proxy or u: 'udp://1.1.1.1:53' outbound: proxy | the arrow form is rejected (a declaration cannot carry ->); the outbound: proxy form is two adjacent declarations | honk extension: both forms make upstream u dial through outbound proxy; both suffixes are read only outside quoted URI data. | dialect-dns-05-arrow-detour, dialect-dns-05-key-detour |
Text after a matcher call, dport(443)junk -> proxy, qname(a.example)junk -> reject | rejected: the arrow must follow the call | Traffic rejects the configuration with located trailing-matcher-text; DNS warns and omits the whole rule. Remove trailing matcher text. | dialect-dns-06-traffic-junk, dialect-dns-06-dns-junk |
A trailing comment on an upstream line, v: 'udp://8.8.8.8:53' # note | comment | An unquoted token-head # starts a comment, so the address is 8.8.8.8:53; the changed interpretation emits legacy-upstream-comment. Use ordinary trailing # comments; quotes protect address data. | dialect-dns-07-upstream-comment |
-> or outbound: inside a quoted upstream URL | data | Only unquoted descriptor suffixes select detours; 'https://dns.example/q?x=outbound:proxy#frag' stays one URI. A changed old split emits legacy-upstream-separator; put the detour outside URL quotes. | dialect-dns-08-quoted-outbound, dialect-dns-08-quoted-arrow |
qtype(...) | function arguments | Names A, AAAA, CNAME, MX, TXT, NS, PTR, SOA, SRV, HTTPS, SVCB, ANY, * (case-insensitive) or decimal u16; unknown names emit invalid-qtype and omit the whole rule, including mixed or negated lists. Correct unknown names or use their numeric code. Explicit qtype() remains match-nothing; qtype('a,aaaa') still selects both types with legacy-quoted-list. Prefer bare or individually quoted items. | dialect-dns-09-names, dialect-dns-09-1-28, dialect-dns-09-typo, dialect-dns-09-aggregate, dialect-dns-09-empty |
Groups
| Input | dae grammar | honk | Cases |
|---|---|---|---|
group('hk,jp') | one literal | Two subgroup tags, split on , and |. | dialect-groups-01-aggregate, dialect-groups-01-pipe |
filter: group() | call without parameters | Retained as an explicit empty contribution through serde and refresh, with empty-subgroup. Remove the filter for all nodes; explicit group() now selects none. | dialect-groups-02-empty |
Sections and includes
| Input | dae grammar | honk | Cases |
|---|---|---|---|
Unknown content in experimental | any declaration | Unknown outer settings and unsupported legacy udp_nfqueue content, including nested blocks, are errors. Unknown scalar keys inside clash_api and cache_file receive unknown-key and are ignored; this forward-compatibility boundary does not weaken NFQUEUE validation. | dialect-sections-01-outer-key, dialect-sections-01-nfqueue-block, dialect-sections-01-child-key |
Unknown nested blocks in global, dns, routing, group settings, or recognized experimental children | any expression | One unknown-block warning at the header; skip the complete balanced subtree, including DNS child contexts. Descendant settings and rules no longer leak into the parent. Move known settings to their documented block level. | dialect-sections-02-global-wrapper, dialect-sections-02-dns-wrapper, dialect-sections-02-routing-wrapper, dialect-sections-02-group-wrapper |
| Node/subscription wrappers and nested subscription-setting wrappers | not a separate wrapper feature | Retained bounded compatibility traversal, with legacy-wrapper at each wrapper. Arbitrary names at group root are real groups, not wrappers. A subscription header with a quoted URL, tag: 'url' { … }, is an entry with options, not a wrapper: entry-shaped children inside it are ignored with unknown-key. Wrapper compatibility will be reviewed after the first PR2 release. | dialect-sections-03-node-wrapper, dialect-sections-03-sub-wrapper |
| Unknown root statements and scalar keys | arbitrary identifiers | Warn and omit after structure recognition; unknown root blocks are skipped as complete subtrees. Names of groups, entry tags, upstreams and TTL owners are dynamic names, not unknown schema keys. Traffic lines without arrows are diagnosed and omitted unless they declare a fallback. | dialect-sections-04-root-statement, dialect-sections-04-unknown-key, dialect-sections-04-unknown-root, dialect-sections-04-traffic-statement |
An assets { … } root | not a dae section | honk extension: download defaults for geodata, the external UI and subscriptions; see the assets reference. The five experimental download keys it replaces remain accepted, each occurrence with a legacy-assets-key warning. | |
include { … } | a block like any other | Patterns expand only on file loading, with the ordering and confinement rules in the configuration guide; string parsing checks structure without opening files. Adjacent quoted paths such as 'first.dae''quoted # name.dae' retain quoted hashes and braces during initial token acquisition, in both modes. Every quote must close on its physical line. Only token-head # starts a comment; absolute.dae#note is a literal pattern with legacy-include-hash, and normal .dae filtering still applies. | dialect-sections-05-include, dialect-sections-05-adjacent, dialect-sections-05-hash |
Source: honk upstream, licensed under GPL-3.0-only.
