Убран прокси для выхода в интернет: клиентам не нужен (прокси в нашей среде — только для работы ассистента); предпроверка установки с образа без сети — по-прежнему не останавливает установку

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-10-05 17:43:52 +03:00
parent 10305a095e
commit 7a5673e91f
9 changed files with 13 additions and 190 deletions
+2 -3
View File
@@ -232,15 +232,14 @@ func (in *installer) stepPrecheck() (string, error) {
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
// прокси организации — из окружения (https_proxy / HTTPS_PROXY), как у curl
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, in.server+"/healthz", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
if in.bundle != "" {
// установка с образа — всё нужное на диске; ключ продукта и обновления — позже, когда будет связь
return fmt.Sprintf("%s, %.1f ГБ памяти; нет связи с %s — ставим с образа, ключ продукта введёте в браузере, когда появится доступ (можно через прокси: «Настройка → Сервер»)", name, float64(mem)/(1<<30), in.server), nil
return fmt.Sprintf("%s, %.1f ГБ памяти; нет связи с %s — ставим с образа, ключ продукта введёте в браузере, когда появится связь", name, float64(mem)/(1<<30), in.server), nil
}
return "", fmt.Errorf("нет связи с %s: %v (если выход в интернет через прокси — запустите с переменной https_proxy=http://прокси:порт)", in.server, err)
return "", fmt.Errorf("нет связи с %s: %v", in.server, err)
}
resp.Body.Close()
return fmt.Sprintf("%s, %.1f ГБ памяти, связь с сервером лицензий есть", name, float64(mem)/(1<<30)), nil
+3 -5
View File
@@ -114,11 +114,9 @@ PY
st deps "системные пакеты для работы ИШ без графики"
need=""
for p in gnome-keyring dbus xvfb xauth libsecret-tools unzip curl; do dpkg -s "$p" >/dev/null 2>&1 || need="$need $p"; done
px=$(j proxy) # прокси организации (из настроек bj) — для apt и скачивания с nsd.ru
aptpx=(); [ -n "$px" ] && aptpx=(-o "Acquire::http::Proxy=$px" -o "Acquire::https::Proxy=$px")
if [ -n "$need" ]; then
DEBIAN_FRONTEND=noninteractive apt-get "${aptpx[@]}" install -y -qq --no-install-recommends $need || \
{ DEBIAN_FRONTEND=noninteractive apt-get "${aptpx[@]}" update -qq && DEBIAN_FRONTEND=noninteractive apt-get "${aptpx[@]}" install -y -qq --no-install-recommends $need; } || die "не удалось установить пакеты:$need"
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends $need || \
{ DEBIAN_FRONTEND=noninteractive apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends $need; } || die "не удалось установить пакеты:$need"
fi
st package "дистрибутив ИШ"
@@ -127,7 +125,7 @@ PY
v=$(printf '%s' "$ver" | tr . -)
url="https://www.nsd.ru/media/integratsionnyj-shlyuz/$v/igate-$v-amd64.zip"
echo "скачивание $url"
curl -fsSL -m 600 ${px:+-x "$px"} -A 'Mozilla/5.0 (X11; Linux x86_64) bj-installer' -H 'Accept: */*' -o "$W/igate.zip" "$url" || die "не удалось скачать дистрибутив ИШ $ver с сайта НРД ($url)"
curl -fsSL -m 600 -A 'Mozilla/5.0 (X11; Linux x86_64) bj-installer' -H 'Accept: */*' -o "$W/igate.zip" "$url" || die "не удалось скачать дистрибутив ИШ $ver с сайта НРД ($url)"
file="$W/igate.zip"
fi
case "$file" in
-3
View File
@@ -156,9 +156,6 @@ func registerIgateAdmin(mux *http.ServeMux, rc *RuntimeConfig) {
jsonOut(w, map[string]any{"ok": false, "error": "загрузите архив ИШ или укажите версию вида 107.0.930"})
return
}
if u := rc.Snapshot().Proxy.url(); u != nil {
params["proxy"] = u.String() // скачивание с nsd.ru и пакеты — через прокси организации
}
extra, _ := json.Marshal(params)
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
defer cancel()
-13
View File
@@ -164,19 +164,6 @@ func TestBuildRequestNeedsOrganization(t *testing.T) {
}
}
func TestProxyBypass(t *testing.T) {
for host, want := range map[string]bool{"localhost": true, "10.10.10.21": true, "192.168.1.5": true, "127.0.0.1": true, "sql002": true, "bj.local": true,
"lic.zetit.ru": false, "www.nsd.ru": false, "8.8.8.8": false, "corp.example.ru": true, "a.corp.example.ru": true} {
if got := proxyBypass(host, "corp.example.ru"); got != want {
t.Errorf("proxyBypass(%s)=%v, want %v", host, got, want)
}
}
p := ProxySettings{Address: "proxy.example.ru:3128", User: "u", Password: "p@ss"}
if u := p.url(); u == nil || u.String() != "http://u:p%40ss@proxy.example.ru:3128" {
t.Errorf("url: %v", u)
}
}
func TestKeyArchiveContour(t *testing.T) {
dir := t.TempDir()
raw, err := nsdCertsFS.ReadFile("nsd-certs/nsd-test-gost-petrov-20240924.cer")
-131
View File
@@ -1,131 +0,0 @@
package lkgateway
// Прокси для выхода в интернет (05.10.2026). Во многих организациях сервер ходит в интернет только через
// прокси: без него не активировать ключ продукта, не получить обновления, не скачать ИШ и документы НРД.
// Прокси из настроек действует на всё, что bj открывает в интернете; адреса локальной сети (шлюз НРД, учёт,
// модули, свой кабинет) — напрямую. Не задан — как раньше (переменные окружения службы).
import (
"context"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"time"
)
// ProxySettings — прокси организации.
type ProxySettings struct {
Address string `json:"address,omitempty"` // http://proxy.corp.local:3128
User string `json:"user,omitempty"`
Password string `json:"password,omitempty"`
NoProxy string `json:"no_proxy,omitempty"` // ещё адреса напрямую, через запятую: corp.local, 10.20.0.5
}
// url — адрес прокси с логином (nil — прокси не задан).
func (p ProxySettings) url() *url.URL {
a := strings.TrimSpace(p.Address)
if a == "" {
return nil
}
if !strings.Contains(a, "://") {
a = "http://" + a
}
u, err := url.Parse(a)
if err != nil || u.Host == "" {
return nil
}
if p.User != "" {
u.User = url.UserPassword(p.User, p.Password)
}
return u
}
// proxyBypass — адрес локальной сети или из списка «напрямую».
func proxyBypass(host, extra string) bool {
host = strings.ToLower(strings.TrimSpace(host))
if host == "" || host == "localhost" || !strings.Contains(host, ".") || strings.HasSuffix(host, ".local") {
return true
}
if ip := net.ParseIP(host); ip != nil {
return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast()
}
for _, x := range strings.Split(extra, ",") {
x = strings.ToLower(strings.TrimPrefix(strings.TrimSpace(x), "."))
if x != "" && (host == x || strings.HasSuffix(host, "."+x)) {
return true
}
}
return false
}
// proxyFunc — прокси из настроек bj (без них — напрямую).
func proxyFunc(rc *RuntimeConfig) func(*http.Request) (*url.URL, error) {
return func(req *http.Request) (*url.URL, error) {
p := rc.Snapshot().Proxy
u := p.url()
if u == nil || proxyBypass(req.URL.Hostname(), p.NoProxy) {
return nil, nil
}
return u, nil
}
}
// applyProxy — прокси настроек для всех HTTP-запросов bj (сервер лицензий, обновления, сайт НРД).
func applyProxy(rc *RuntimeConfig) {
if t, ok := http.DefaultTransport.(*http.Transport); ok {
env, mine := t.Proxy, proxyFunc(rc)
t.Proxy = func(req *http.Request) (*url.URL, error) {
if rc.Snapshot().Proxy.url() != nil {
return mine(req)
}
if env != nil {
return env(req)
}
return nil, nil
}
}
if t, ok := noProxyClient.Transport.(*http.Transport); ok {
t.Proxy = proxyFunc(rc) // документы НРД: прокси окружения не берём (он может блокировать nsd.ru), прокси bj — да
}
}
func registerProxy(mux *http.ServeMux, rc *RuntimeConfig) {
mux.HandleFunc("/admin/setup/proxy", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
p := rc.Snapshot().Proxy
p.Address = strings.TrimSpace(r.FormValue("address"))
p.User = strings.TrimSpace(r.FormValue("user"))
if pw := r.FormValue("password"); pw != "" || p.User == "" {
p.Password = pw // пароль в страницу не выводится: пусто — прежний
}
p.NoProxy = strings.TrimSpace(r.FormValue("no_proxy"))
if p.Address != "" && p.url() == nil {
setupFlash(w, r, "Прокси: адрес вида http://proxy.example.ru:3128")
return
}
if err := rc.UpdateProxy(p); err != nil {
setupFlash(w, r, "Прокси: "+err.Error())
return
}
if p.Address == "" {
setupFlash(w, r, "Прокси выключен: bj выходит в интернет напрямую")
return
}
// проверка: сервер лицензий через прокси
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://lic.zetit.ru/healthz", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
setupFlash(w, r, "Прокси сохранён, но сервер лицензий через него недоступен: "+err.Error())
return
}
resp.Body.Close()
setupFlash(w, r, fmt.Sprintf("Прокси сохранён: сервер лицензий через него отвечает (HTTP %d)", resp.StatusCode))
})
}
+1 -3
View File
@@ -296,9 +296,7 @@ func readyNetwork(ctx context.Context, s Settings) ReadyCheck {
ts = append(ts, target{"НРД (TEST3, ГОСТ)", "gost-t3.nsd.ru:443"}, target{"справочник Мосбиржи (LDAP)", "vcert.pki.moex.com:50005"})
}
}
if strings.TrimSpace(s.Proxy.Address) == "" { // через прокси прямого TCP не будет — сервер лицензий проверяет его работа
ts = append(ts, target{"сервер лицензий", "lic.zetit.ru:443"})
}
ts = append(ts, target{"сервер лицензий", "lic.zetit.ru:443"})
var bad, good []string
var mu sync.Mutex
var wg sync.WaitGroup
-10
View File
@@ -42,7 +42,6 @@ type Settings struct {
Backup BackupSettings `json:"backup,omitempty"`
HTTPS HTTPSSettings `json:"https,omitempty"`
Time TimeSettings `json:"time,omitempty"`
Proxy ProxySettings `json:"proxy,omitempty"`
LastTest *TestRunResult `json:"last_test,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -481,15 +480,6 @@ func (r *RuntimeConfig) UpdateHTTPS(s HTTPSSettings) error {
return r.save()
}
// UpdateProxy сохраняет прокси для выхода в интернет.
func (r *RuntimeConfig) UpdateProxy(s ProxySettings) error {
r.mu.Lock()
r.data.Proxy = s
r.data.UpdatedAt = time.Now()
r.mu.Unlock()
return r.save()
}
// UpdateTime сохраняет настройки синхронизации времени.
func (r *RuntimeConfig) UpdateTime(s TimeSettings) error {
r.mu.Lock()
+7 -9
View File
@@ -214,15 +214,13 @@ func NewServer(cfg ServerConfig) (*Server, error) {
registerOutgoing(mux, adminTpl, outDeps)
registerManual(mux, rc, svc) // ручной ввод исходящих переводов по бумажным поручениям
registerJournal(mux, adminTpl, svc, incomingStore)
registerNSDHealth(mux, rc) // связь с НРД: шапка (json) и «Проверить связь сейчас»
registerBackup(mux, rc) // резервное копирование и восстановление (backup.go)
registerIgate(mux, rc) // интеграционный шлюз НРД: состояние и установка (igate.go)
registerReadiness(mux, rc) // итоговая проверка готовности (мастер, шаг 10)
registerQuikReady(mux, rc) // проверка модуля неторговых поручений QUIK (только чтение)
registerHTTPS(mux, rc) // HTTPS веб-интерфейса: перенаправление, свой сертификат
registerTime(mux, rc) // сервер времени (NTP)
registerProxy(mux, rc) // прокси для выхода в интернет
applyProxy(rc)
registerNSDHealth(mux, rc) // связь с НРД: шапка (json) и «Проверить связь сейчас»
registerBackup(mux, rc) // резервное копирование и восстановление (backup.go)
registerIgate(mux, rc) // интеграционный шлюз НРД: состояние и установка (igate.go)
registerReadiness(mux, rc) // итоговая проверка готовности (мастер, шаг 10)
registerQuikReady(mux, rc) // проверка модуля неторговых поручений QUIK (только чтение)
registerHTTPS(mux, rc) // HTTPS веб-интерфейса: перенаправление, свой сертификат
registerTime(mux, rc) // сервер времени (NTP)
registerLKAPI(mux, rc, svc) // API для модуля личного кабинета клиента (bj-lk)
registerHealth(mux)
registerQuikOps(mux, rc, svc)
@@ -966,19 +966,6 @@
<button type="submit" class="btn">Применить</button>
</form>
</div>
<div class="card" id="sec-proxy">
<h2>Прокси для выхода в интернет</h2>
<p class="muted">Если сервер выходит в интернет только через прокси: через него bj обращается к серверу лицензий (ключ продукта, продление, обновления), к сайту НРД (документы, дистрибутив ИШ). Адреса локальной сети — шлюз, учёт, модули — всегда напрямую. Пусто — без прокси.</p>
<form method="post" action="/admin/setup/proxy" style="display:grid;gap:10px;max-width:640px">
<div style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:10px">
<label style="margin:0">Адрес прокси<input name="address" value="{{.Settings.Proxy.Address}}" placeholder="http://proxy.example.ru:3128" style="width:100%;margin-top:4px"></label>
<label style="margin:0">Логин<input name="user" value="{{.Settings.Proxy.User}}" autocomplete="off" style="width:100%;margin-top:4px"></label>
<label style="margin:0">Пароль {{if .Settings.Proxy.Password}}<span class="muted">(задан)</span>{{end}}<input name="password" type="password" autocomplete="new-password" style="width:100%;margin-top:4px"></label>
</div>
<label style="margin:0">Ещё адреса напрямую (через запятую) <span class="muted">— локальная сеть и *.local уже напрямую</span><input name="no_proxy" value="{{.Settings.Proxy.NoProxy}}" placeholder="corp.example.ru" style="width:100%;margin-top:4px"></label>
<div><button type="submit" class="btn">Сохранить и проверить</button></div>
</form>
</div>
<div class="card" id="sec-network">
<h2>Сеть</h2>
<p style="margin:6px 0">Сервер: <code>{{serverAddrs}}</code></p>