Skip to content

fix: daemon lifecycle, resource hardening and security (1.1.3) - #8

Merged
luynrs merged 11 commits into
mainfrom
fix/core
Aug 28, 2026
Merged

fix: daemon lifecycle, resource hardening and security (1.1.3)#8
luynrs merged 11 commits into
mainfrom
fix/core

Conversation

@luynrs

@luynrs luynrs commented Aug 28, 2026

Copy link
Copy Markdown
Owner
  • graceful daemon shutdown in installers via jray down
  • strict bootstrap DNS in probe to prevent leaks
  • reliable socket lock release and safe elevated restart
  • guaranteed TUN and route cleanup on engine start failure

@luynrs
luynrs merged commit 7497a4f into main Aug 28, 2026
1 check passed
@luynrs
luynrs deleted the fix/core branch August 28, 2026 15:52
luynrs added a commit that referenced this pull request Aug 30, 2026
> **Параллельно выполнять работу — можно. Параллельно изменять состояние
  системы — нельзя.**

Это даст одновременно производительность, современную архитектуру и
сильно меньшую поверхность для гонок/рассинхронизации.

```text
CLI / TUI
│
│ RPC
▼
┌───────────────────────────────┐
│            Daemon             │
│                               │
│        ┌─────────────┐        │
│        │    Core     │        │
│        │ single owner│        │
│        │ of State    │        │
│        └──────┬──────┘        │
│               │               │
│      ┌────────┼─────────┐     │
│      ▼        ▼         ▼     │
│    Engine   Store     Workers │
│                       │       │
│                 HTTP / Probe  │
│                 DNS / Refresh │
│                               │
└───────────────────────────────┘
```

Сам `Core` не обязан быть одним большим goroutine-actor с сотней
сообщений. Я бы сделал обычный объект с одним `opMu` для mutations и
`RWMutex` либо atomic snapshot для чтения. Это проще actor model и легче
отлаживается.

Ключевое — **никакой другой компонент не меняет business state
самостоятельно**.

Например:

```go
type Core struct {
opMu sync.Mutex

    state atomic.Pointer[Snapshot]

    engine Engine
    store  Store
    }
    ```

Для чтений:

```go
func (c *Core) Snapshot() Snapshot {
return *c.state.Load()
}
```

Для mutations:

```go
func (c *Core) Connect(ctx context.Context, ref NodeRef) (Snapshot,
error) {
c.opMu.Lock()
defer c.opMu.Unlock()

    // transition
    }
    ```

Это очень быстрый и при этом скучный concurrency design.

---

Я бы перестал распределять authoritative состояние по:

```text
connection.Service
subscription.Service
store
watch
TUI
```

и определил один объект:

```go
type Snapshot struct {
Revision uint64

    Settings      Settings
    Subscriptions []Subscription
    Nodes         []Node

    Connection ConnectionState
    }
    ```

Daemon всегда знает:

> текущее подтверждённое состояние системы — вот этот Snapshot.

После успешного изменения:

```text
old Snapshot
↓
compute desired
↓
perform external work
↓
persist
↓
publish new Snapshot
```

Снаружи Snapshot меняется **целиком**.

Это сильно упрощает reasoning.

---

Я бы не давал Core напрямую оркестрировать:

```text
Start
Swap
TunAdd
TunRemove
Restart
...
```

Его API должен быть примерно таким:

```go
type Engine interface {
Apply(context.Context, SessionSpec) error
Stop(context.Context) error
}
```

А внутри engine уже решает:

```text
можно hot swap?
→ swap

нужно только переключить TUN?
→ tun transition

нужен полный restart?
→ restart
```

Таким образом оптимизации остаются.

Но Core видит одну операцию:

```go
engine.Apply(ctx, desired)
```

Это важный уровень абстракции.

Тебе не приходится через два года помнить:

> «после Swap обязательно сделать X, если TUN поменялся, но перед Y,
  кроме случая elevation».

Такое знание живёт внутри одного компонента.

---

Например:

```go
type SessionSpec struct {
Node NodeRef
Tun  bool

    ListenPort int
    DNS        DNSSettings
    Routing    RoutingSettings
    }
    ```

Тогда engine state является функцией:

```text
SessionSpec → running engine
```

А не результатом истории:

```text
Start(A)
TunAdd()
SetPort()
Swap(B)
TunRemove()
...
```

Это огромное упрощение.

История операций перестаёт иметь значение.

---

Я бы убрал API вида:

```go
SetTun()
SetActive()
SetSettings()
SetWhatever()
```

и оставил:

```go
type Store interface {
Load() (PersistentState, error)
Save(PersistentState) error
}
```

То есть один atomic persistence snapshot.

Например:

```go
type PersistentState struct {
Settings      Settings
Subscriptions []Subscription
Desired       *SessionSpec
}
```

Это очень сильно уменьшит partial commit bugs.

Вместо:

```text
SetTun success
SetActive fail
SetSettings success
```

есть:

```text
Save(nextState)
```

одна transactional boundary.

С YAML это особенно удобно: temp file → fsync → rename.

---

Важно различать:

Последовательная.

Параллельная.

Refresh:

```text
┌── fetch #1
├── fetch #2
Core ─ job ───┼── fetch #3
├── ...
└── fetch #8
│
▼
results
│
▼
Core
commit
```

Probe:

```text
32 concurrent probes
↓
ProbeReport
```

DNS:

```text
cache + singleflight
```

Парсинг subscriptions тоже можно делать worker'ами.

Получается важный принцип:

> **Workers возвращают данные. Workers никогда не меняют State.**

Это как раз очень современная и хорошо масштабируемая модель.

---

Не нужно городить generations для этого случая.

Просто:

```text
refresh sub A already running
↓
second request joins existing job
```

То есть `singleflight`.

Это одновременно:

* быстрее;
* проще;
* меньше HTTP;
* нет stale commits;
* нет generation bookkeeping.

Для `RefreshAll` тот же worker pool может переиспользоваться.

---

Probe вообще не обязательно смешивать с глобальным состоянием daemon.

```go
Probe(ctx, refs) (ProbeReport, error)
```

Он может работать на 32/64 goroutines.

Результат получает TUI.

Если пользователь запускает новый Probe:

```text
cancel previous
start new
```

Очень простая модель.

Не надо:

```text
probe generations
probe revisions
probe persisted state
probe events
```

---

Не polling.

Не полноценный event bus.

Не поток состояний.

А:

```go
type Changed struct {
Revision uint64
}
```

Daemon говорит только:

> состояние изменилось, теперь revision 42.

TUI:

```text
имею 40
получил 42
↓
Snapshot()
```

Всё.

Потерял event 41?

Вообще неважно.

Watch отвалился?

Reconnect → Snapshot.

Notification пришёл после ответа Connect?

Revision уже та же → игнорируем.

Channel переполнился?

Пускай теряет события — главное, чтобы когда-нибудь пришёл новый
revision.

Это практически идеальная semantics для такой программы.

---

Я бы стремился примерно к этому набору:

```text
Hello
Snapshot

Connect(NodeRef)
Disconnect
SetSettings(Settings)

AddSubscription(...)
RemoveSubscription(...)
Refresh(...)

Probe(...)

Watch
Shutdown
```

И каждая state-changing операция возвращает:

```go
type MutationResult struct {
Snapshot Snapshot
}
```

Не надо отдельного `Status()` после операции.

---

Очень важное правило.

Любая операция:

```text
request
↓
work
↓
commit
↓
response
```

либо:

```text
request
↓
error
```

Никакого:

```text
вернули клиенту
а daemon там ещё что-то доделывает
```

кроме явно background вещей вроде AutoRefresh.

Даже background jobs должны иметь owner и context.

---

```text
daemonCtx
│
├── rpcRequestCtx
│    └── engine / HTTP / DNS
│
├── autoRefreshCtx
│
└── watchCtx
```

`context.Background()` появляется только в `main`.

После `main` он исчезает из application code.

Очень простое правило для code review:

> увидел `context.Background()` ниже composition root — почти наверняка
  неправильно.

---

```text
cancel daemonCtx
↓
stop accepting RPC
↓
workers receive cancellation
↓
engine.Stop(ctx)
↓
bounded wait
↓
exit
```

Например:

```text
graceful deadline: 5–10 sec
```

После него force close.

Не должно существовать ни одной daemon goroutine без владельца.

---

Примерно:

```text
internal/
core/
core.go
state.go
connection.go
subscriptions.go

    engine/
    engine.go
    singbox/

    store/
    store.go
    yaml.go

    jobs/
    refresh.go
    probe.go

    daemon/
    rpc/
    lifecycle/

    client/
    rpc/
    cli/
    tui/

    domain/
    node.go
    subscription.go
    settings.go
    ```

Причём:

Владеет semantics.

Ничего не знает про UI/RPC/store.

Ничего не знает про engine.

Делают I/O и возвращают результаты.

Просто transport.

Просто отображает Snapshot и запускает commands.

Вот это хороший уровень разделения.

---

Не стал бы добавлять:

* event sourcing;
* полноценную actor system;
* CQRS;
* несколько command queues;
* database;
* сложный transaction framework;
* distributed-style retry logic;
* десятки mutex'ов;
* универсальный generic job scheduler;
* огромный internal event bus.

Для JustRay это будет архитектурная инфляция.

Тебе не нужна система, способная обслуживать миллион requests в секунду.

Тебе нужна система, которая очень быстро управляет одним локальным
networking engine и при этом **остаётся очевидной через три года**.

---

В hot path:

```text
Snapshot read → atomic load
```

Практически бесплатно.

Параллельный I/O:

```text
bounded goroutines
```

Быстро.

Mutation:

```text
один opMu
```

Просто.

Persistence:

```text
один atomic snapshot save
```

Надёжно.

Engine:

```text
one desired SessionSpec
```

Предсказуемо.

UI synchronization:

```text
revision notification + Snapshot
```

Минимум гонок.

---

Для любого нового feature можно задать четыре вопроса:

```text
1. Это меняет authoritative State?
   → только через Core.

2. Это тяжёлый I/O?
   → worker/context, результат обратно Core.

3. Клиенту нужно узнать об изменении?
   → revision changed.

4. Это должно пережить restart?
   → поле PersistentState.
   ```

Если разработчик не может определить, куда относится новая вещь, значит
abstraction, скорее всего, неправильная.

Вот **такую** архитектуру я бы рекомендовал тебе как конечную: не
«простую игрушечную», а **boring high-performance architecture** —
минимум механизмов, сильные invariants, параллельность только там, где
она реально даёт скорость, и очень мало возможностей создать гонку
случайным новым кодом.
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.

1 participant