diff --git a/HISTORY.md b/HISTORY.md index bc7f754..78369a8 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2832,3 +2832,46 @@ MikroTik RouterOS, Linux), кнопка «За зразок» на кожном пошук за назвою/виробником/командою, «лише власні», «Cisco IOS» зі зразка заповнив усі шість полів ``` + +## Транспорт вирішує доступ, а не профіль + +### Питання, яке виявило помилку + +«Чому всі готові профілі — ssh? Хіба не має бути telnet?» + +Сам список правильний: `ssh` у всіх 147 вбудованих — це не недогляд, а +розумний типовий вибір. Помилка була в тому, **що саме це поле робило**. + +`buildConfigJob` брав транспорт із профілю моделі. А оскільки профіль +описує модель, а не конкретну коробку, виходило так: хост зі старою +прошивкою, де є лише telnet, і з чесно заведеним telnet-доступом усе +одно набирався по SSH — бо профіль моделі каже «ssh». І не збирався +ніколи. + +Доступ при цьому вже ніс потрібне поле: `inv.credentials.proto` — саме +воно й відповідає на питання «чим до цієї коробки достукатись». Воно +доїжджало до зонда в `Credential.Transport` і не використовувалось для +набору. + +### Як тепер + +Транспорт бере доступ. Профільне поле лишилось підказкою «чим це залізо +зазвичай беруть» і діє, тільки поки доступу немає. Порт іде за +транспортом (22/23), а порт із доступу перекриває типовий — залізо за +NAT цілком може слухати SSH на 2222. + +Логіка винесена в `jobTransport()` окремою функцією саме щоб її можна +було перевірити без бази й без пристрою: чотири випадки, чотири рядки +таблиці. + +У формі профілю поле перейменоване на «Типовий транспорт» із підказкою, +що реальний задає доступ хоста. Назва колонки — «Типово». + +### Перевірено + +``` +доступ telnet + профіль ssh → telnet, порт 23 +доступ ssh + профіль telnet → ssh, порт 22 +порт 2222 у доступі → 2222 +без доступу → лишається підказка профілю +``` diff --git a/server/internal/store/ncm_jobs.go b/server/internal/store/ncm_jobs.go index 65acb4d..0f22f2b 100644 --- a/server/internal/store/ncm_jobs.go +++ b/server/internal/store/ncm_jobs.go @@ -160,15 +160,7 @@ func (s *Store) buildConfigJob(ctx context.Context, tenantID, deviceID, jobID st return nil, err } - transport := npv1.Transport_TRANSPORT_SSH - port := uint32(22) - if prof.Transport == "telnet" { - transport = npv1.Transport_TRANSPORT_TELNET - port = 23 - } - if cred != nil && cred.Port != 0 { - port = cred.Port - } + transport, port := jobTransport(prof.Transport, cred) return &npv1.ConfigJob{ JobId: jobID, @@ -394,3 +386,36 @@ func (s *Store) ListConfigJobs(ctx context.Context, tenantID, deviceID string, l }) return out, err } + +// jobTransport каже, чим набирати цей хост і на який порт. +// +// Транспорт вирішує ДОСТУП, а не профіль. Профіль описує модель заліза: +// які команди віддати й як упізнати запрошення. А чим до конкретної +// коробки достукатись — властивість самої коробки: та сама модель у +// клієнта може стояти з увімкненим SSH, а на сусідньому вузлі — зі +// старою прошивкою, де є лише telnet. +// +// Раніше вирішував профіль, і оскільки всі 147 вбудованих кажуть «ssh», +// хост із telnet-доступом набирався по SSH і не збирався ніколи. +// Профільне поле лишилось підказкою «чим це залізо зазвичай беруть» і +// діє, тільки поки доступу немає. +func jobTransport(profileTransport string, cred *npv1.Credential) (npv1.Transport, uint32) { + transport := npv1.Transport_TRANSPORT_SSH + switch { + case cred != nil: + transport = cred.Transport + case profileTransport == "telnet": + transport = npv1.Transport_TRANSPORT_TELNET + } + + port := uint32(22) + if transport == npv1.Transport_TRANSPORT_TELNET { + port = 23 + } + // Порт із доступу перекриває типовий: залізо за NAT цілком може + // слухати SSH на 2222. + if cred != nil && cred.Port != 0 { + port = cred.Port + } + return transport, port +} diff --git a/server/internal/store/ncm_transport_test.go b/server/internal/store/ncm_transport_test.go new file mode 100644 index 0000000..4b8d4b3 --- /dev/null +++ b/server/internal/store/ncm_transport_test.go @@ -0,0 +1,64 @@ +package store + +import ( + "testing" + + npv1 "github.com/netpulse/netpulse/gen/go/netpulse/v1" +) + +// Транспорт задає доступ хоста, а не профіль моделі. +// +// Усі 147 вбудованих профілів кажуть «ssh», бо так знімають з +// переважної більшості заліза. Але та сама модель у клієнта може стояти +// зі старою прошивкою, де є лише telnet, — і поки вирішував профіль, +// такий хост набирався по SSH і не збирався ніколи. +func TestTransportComesFromCredential(t *testing.T) { + cases := []struct { + name string + profile string + cred *npv1.Credential + want npv1.Transport + port uint32 + }{ + { + name: "доступ telnet перемагає профіль ssh", + profile: "ssh", + cred: &npv1.Credential{Transport: npv1.Transport_TRANSPORT_TELNET}, + want: npv1.Transport_TRANSPORT_TELNET, + port: 23, + }, + { + name: "доступ ssh перемагає профіль telnet", + profile: "telnet", + cred: &npv1.Credential{Transport: npv1.Transport_TRANSPORT_SSH}, + want: npv1.Transport_TRANSPORT_SSH, + port: 22, + }, + { + name: "порт з доступу перекриває типовий", + profile: "ssh", + cred: &npv1.Credential{Transport: npv1.Transport_TRANSPORT_SSH, Port: 2222}, + want: npv1.Transport_TRANSPORT_SSH, + port: 2222, + }, + { + name: "без доступу лишається підказка профілю", + profile: "telnet", + cred: nil, + want: npv1.Transport_TRANSPORT_TELNET, + port: 23, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got, port := jobTransport(c.profile, c.cred) + if got != c.want { + t.Fatalf("транспорт: %v, очікував %v", got, c.want) + } + if port != c.port { + t.Fatalf("порт: %d, очікував %d", port, c.port) + } + }) + } +} diff --git a/web/src/pages/ProfilesPage.tsx b/web/src/pages/ProfilesPage.tsx index 658150f..79b38ca 100644 --- a/web/src/pages/ProfilesPage.tsx +++ b/web/src/pages/ProfilesPage.tsx @@ -208,7 +208,7 @@ export function ProfilesPage() { }, { key: 'transport', - header: 'Транспорт', + header: 'Типово', cell: (p) => {p.transport}, }, { @@ -413,7 +413,10 @@ function ProfileForm({ onChange={(e) => setVendor(e.target.value)} /> - +