Skip to content

Add opt-in, fail-closed Tailnet binding for the local API server - #22

Open
hmmhmmhm wants to merge 3 commits into
drumih:mainfrom
hmmhmmhm:main
Open

Add opt-in, fail-closed Tailnet binding for the local API server#22
hmmhmmhm wants to merge 3 commits into
drumih:mainfrom
hmmhmmhm:main

Conversation

@hmmhmmhm

Copy link
Copy Markdown

Summary

Allow TurboFieldfareServer to serve trusted devices in the same Tailnet without opening the API to the local LAN or requiring a separate proxy.

The server currently binds exclusively to loopback. That is the safest default, but it also prevents using the model from another personally controlled device. A generic --host option or 0.0.0.0 binding would make accidental exposure too easy, so this change intentionally provides a narrower option:

TurboFieldfareServer \
  --model scratch/gemma4.gturbo \
  --bind tailnet

When --bind tailnet is selected, the server:

  • obtains the machine IPv4 address from tailscale ip -4;
  • binds only to that exact Tailnet address;
  • never binds to 0.0.0.0 or another wildcard interface;
  • fails instead of falling back when Tailscale cannot be detected.

The default remains loopback, so existing commands and their security properties are unchanged.

The README, server guide, and agent instructions now document the same safety boundary: Tailnet binding is explicit, relies on the Tailnet ACL, and does not add application-level authentication or TLS.

Validation

Passed:

Scripts/test.sh --filter ServerArgumentTests
swift build -c release --product TurboFieldfareServer
ruby Scripts/check_markdown_links.rb

Results:

  • All 3 server argument tests passed.
  • The release TurboFieldfareServer product built successfully.
  • All 22 Markdown files passed local link and anchor validation.
  • The default bind mode remains loopback.
  • Unsupported bind modes are rejected.

Tailnet-address API smoke test:

GET http://<tailnet-ip>:8080/health
→ {"status":"ok"}

GET http://<tailnet-ip>:8080/v1/models
→ gemma-4-26b-a4b-it

Chat Completions request:

Prompt: 대한민국의 수도는 어디인가요? 한 문장으로 답하세요.
Output: 대한민국의 수도는 서울입니다.
Usage: 29 prompt tokens, 9 completion tokens

Test environment:

MacBook Pro (Mac16,7)
Apple M4 Pro, 24 GB
macOS 26.2
Swift 6.3.2

Memory and performance

Not applicable.

This change does not modify model loading, inference, Metal kernels, KV cache, or bounded-memory behavior. It only changes the address passed to the existing SwiftNIO server bootstrap.

Remaining limitations

  • Tailscale must be running and its CLI must be available on PATH.

  • Tailnet detection currently supports IPv4 only.

  • Access control relies on the Tailnet ACL. The API still has no application-level authentication or TLS.

  • The smoke test reached the server through the host Tailnet address. A request originating from a second Tailnet device has not yet been recorded.

  • The complete serial test suite has not yet been run; validation so far used the focused server argument suite.

  • The change does not load a complete checkpoint, shard, or large model tensor into Swift heap memory.

  • Logs and artifacts contain no credentials, private paths, or model weights.

@jezonek

jezonek commented Jul 31, 2026

Copy link
Copy Markdown

Hello, great work! Could you add an option to bind to the selected ip address? Imho listening on 0.0.0.0 is secure enough within LAN.

NeelM0906 added a commit to NeelM0906/Mference that referenced this pull request Aug 1, 2026
MferenceServer defaults to 127.0.0.1 as before. `--bind tailnet` asks the
tailscale CLI for this machine's IPv4 address and binds only that address,
so trusted devices on the same Tailnet can reach the API without exposing
it to the local LAN and without a separate proxy.

Resolution is fail-closed. A missing CLI, a non-zero exit, empty output,
multiple addresses, IPv6-only output, malformed text, and any address
outside Tailscale's 100.64.0.0/10 range all abort startup; none of them
fall back to a wildcard or LAN interface. The address is resolved before
the model loads, so a misconfiguration fails immediately.

Tailnet access is governed by the Tailnet ACL alone; the server still has
no application-level authentication or TLS, and the docs say so.

Ports drumih/turbo-fieldfare#22 by @hmmhmmhm.
@hmmhmmhm

hmmhmmhm commented Aug 3, 2026

Copy link
Copy Markdown
Author

@jezonek I intentionally kept this PR Tailnet-specific because the server has no authentication or TLS.

Binding to 0.0.0.0 would expose it beyond the authenticated Tailnet boundary, so I’d prefer to keep this PR narrowly scoped. A configurable bind address could be discussed separately.

he-be added a commit to he-be/turbo-fieldfare that referenced this pull request Aug 21, 2026
`QwenTokenizer` は `GFTokenizer` に分岐を足すのではなく兄弟にした。Gemma の
`verifyDecoderConfiguration` は「よそのトークナイザを弾く」ために在るので、
そこに ByteLevel を通す穴を開けない。両方向の拒否を検査に入れてある。

- `ByteLevelDecoding` / `ByteLevelRun`: GPT-2 の byte↔unicode 表と、
  「決着した分だけ出す」ストリーミング。落ちた特殊トークンをまたいで
  run は融合する (上流は復号器の前で ID を落とす)
- テンプレートは上流の `chat_template.jinja` をそのまま swift-jinja が描画。
  `--thinking` が `enable_thinking` に直結する
- CLI は `RunQwen.swift` という別経路。family は `manifest.json` から読む。
  `<think>` は ID で判定し、推論は stderr・答えは stdout
- `QwenForwardRunner` / `QwenPrefill` が `beginPhase` を呼ぶようにした
  (decode の要求が prefill 列に入っていた)

検査: `--qwen-tokenizer` 223 本すべて緑 (うち負例 4)。相手は
`Scripts/qwen35/tokenizer_fixture.py` が上流 `tokenizers` / `transformers` から
落とした fixture。`swift test --no-parallel` 1,297 件、Gemma の既定 69 本、
`--qwen` 57 本、`--qwen-prefill` の 55 トークンも緑のまま。

残り: XML 形のツール呼び出しと GBNF (04-PHASES 次の一手 drumih#22)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants