Files
Loki/internal/ajean/sys_network.go
T
nathaninline d881895301 Le drapeau de neutralisation du pare-feu passe cote commun
Declare dans sys_firewall_windows.go, il n'etait qu'ECRIT par les tests
sur les autres plateformes : staticcheck le signalait mort (U1000) et la
CI cassait. Il vit desormais dans sys_network.go, ou setLANExposure le
lit sur les trois plateformes.
2026-08-08 20:05:18 +02:00

178 lines
6.0 KiB
Go

package ajean
import (
"encoding/json"
"fmt"
"net/http"
"strings"
)
// sys_network.go — « est-ce que ma machine est joignable depuis le réseau ? »
//
// Le moteur écoute à l'adresse HOST. Sous Windows, l'installation pose
// HOST=127.0.0.1 : le chat du navigateur marche (il passe par le serveur web
// d'AJEAN, sur la même machine), mais l'endpoint OpenAI :8080/v1 est INVISIBLE
// depuis le reste du réseau. Or c'est très exactement ce que les gens viennent
// chercher : brancher un logiciel tiers sur le modèle qui tourne dans le salon.
//
// Deux choses manquaient, et pas une seule :
// 1. HOST n'était réglable NULLE PART dans l'interface — il fallait connaître
// « ajean edit » et savoir quoi y écrire.
// 2. Même à 0.0.0.0, le pare-feu Windows bloque les connexions entrantes tant
// qu'aucune règle n'autorise le port. AJEAN n'en posait aucune.
//
// D'où un seul interrupteur qui fait les deux, et qui DIT ce qu'il n'a pas pu
// faire (poser une règle de pare-feu exige les droits administrateur, que
// l'installation d'AJEAN ne réclame pas).
// firewallInert coupe tout pilotage du pare-feu. Positionné par les tests : une
// suite qui laisse une règle entrante derrière elle est un effet de bord
// inacceptable, et sur un runner élevé elle ouvrirait le port pour de bon.
// Déclaré ici, et non dans sys_firewall_windows.go, pour être LU sur les trois
// plateformes (sinon il n'est qu'écrit par les tests hors Windows, ce que
// staticcheck signale à juste titre comme une variable morte).
var firewallInert bool
// hostLocalOnly est l'adresse d'écoute « cette machine seulement ».
const hostLocalOnly = "127.0.0.1"
// hostAllInterfaces est l'adresse d'écoute « tout le réseau ».
const hostAllInterfaces = "0.0.0.0"
// engineHost renvoie l'adresse d'écoute configurée pour le moteur. Vide = le
// défaut de backend_serve.go, c'est-à-dire toutes les interfaces.
func engineHost() string {
h := strings.TrimSpace(ReadConfig()["HOST"])
if h == "" {
return hostAllInterfaces
}
return h
}
// lanExposed : le moteur écoute-t-il ailleurs que sur la boucle locale ?
func lanExposed() bool {
h := engineHost()
return h != hostLocalOnly && h != "::1" && !strings.EqualFold(h, "localhost")
}
// netStatus est ce que l'interface affiche, et ce que renvoie `ajean network`.
type netStatus struct {
Exposed bool `json:"exposed"` // le moteur écoute-t-il sur le réseau
Host string `json:"host"` // adresse d'écoute effective
Port int `json:"port"` // port du moteur
URL string `json:"url"` // endpoint OpenAI à coller dans un logiciel tiers
Firewall string `json:"firewall"` // "ouvert", "ferme", "inconnu" (aucun pare-feu piloté)
Hint string `json:"hint"` // ce qu'il reste à faire à la main, ou ""
}
// networkStatus assemble l'état courant.
func networkStatus() netStatus {
st := netStatus{Exposed: lanExposed(), Host: engineHost(), Port: LLMPort()}
st.URL = fmt.Sprintf("http://%s:%d/v1", localIP(), st.Port)
st.Firewall = firewallState(st.Port)
if st.Exposed && st.Firewall == "ferme" {
st.Hint = firewallManualHint(st.Port)
}
return st
}
// setLANExposure écrit HOST et, à l'ouverture, tente de poser la règle de
// pare-feu. Elle renvoie l'état obtenu ; une règle non posée n'est PAS une
// erreur bloquante (l'utilisateur peut la poser lui-même, Hint le lui dit).
//
// Le service n'est pas redémarré ici : llama-server ne lit --host qu'au
// lancement, et c'est à l'appelant de choisir le moment (l'interface propose le
// redémarrage, la CLI le rappelle).
func setLANExposure(on bool) (netStatus, error) {
host := hostLocalOnly
if on {
host = hostAllInterfaces
}
if err := SetConfigKey("HOST", host); err != nil {
return netStatus{}, err
}
port := LLMPort()
switch {
case firewallInert:
// rien : voir la note sur firewallInert
case on:
_ = firewallOpen(port)
default:
_ = firewallClose(port)
}
return networkStatus(), nil
}
// cmdNetwork : `ajean network [on|off|status]`.
func cmdNetwork(args []string) error {
sub := "status"
if len(args) > 0 {
sub = args[0]
}
switch sub {
case "on", "off":
st, err := setLANExposure(sub == "on")
if err != nil {
return err
}
if st.Exposed {
fmt.Printf("%s le moteur écoutera sur tout le réseau (%s)\n", green("[ok]"), st.URL)
} else {
fmt.Printf("%s le moteur n'écoutera plus que sur cette machine\n", green("[ok]"))
}
printNetHint(st)
fmt.Printf("%s redémarre le moteur pour appliquer : %s\n", dim("[info]"), bold("ajean restart"))
return nil
case "status", "":
st := networkStatus()
state := dim("cette machine seulement")
if st.Exposed {
state = green("tout le réseau")
}
fmt.Printf("%s écoute: %s (HOST=%s, PORT=%d)\n", cyan("Réseau"), state, st.Host, st.Port)
switch st.Firewall {
case "ouvert":
fmt.Printf(" pare-feu : %s\n", green("port autorisé"))
case "ferme":
fmt.Printf(" pare-feu : %s\n", yellow("aucune règle pour ce port"))
default:
fmt.Printf(" pare-feu : %s\n", dim("non géré par AJEAN sur cette plateforme"))
}
if st.Exposed {
fmt.Printf(" endpoint OpenAI : %s\n", bold(st.URL))
}
printNetHint(st)
return nil
}
return fmt.Errorf("usage: ajean network [on|off|status]")
}
func printNetHint(st netStatus) {
if st.Hint != "" {
fmt.Printf("%s %s\n", yellow("[attention]"), st.Hint)
}
}
// handleNetwork : GET l'état, POST {exposed:bool} pour basculer.
func handleNetwork(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
var req struct {
Exposed *bool `json:"exposed"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
return
}
if req.Exposed != nil {
st, err := setLANExposure(*req.Exposed)
if err != nil {
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
return
}
sendJSON(w, 200, map[string]any{"ok": true, "status": st})
return
}
}
sendJSON(w, 200, map[string]any{"ok": true, "status": networkStatus()})
}