Files
lpra/internal/server/web/templates/forwards.html
T

149 lines
7.1 KiB
HTML

<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Порты · {{.Agent.Name}} - LPRA</title>
<link rel="stylesheet" href="{{asset "app.css"}}">
<script src="{{asset "pin.js"}}"></script>
<style>
/* One line: fixed narrow fields, the label takes what is left. */
.frow { display:flex; align-items:flex-end; gap:12px; flex-wrap:wrap; }
.frow label { margin:0 0 6px; white-space:nowrap; }
.frow .fld { display:flex; flex-direction:column; flex:none; width:108px; }
.frow .fld.addr { width:128px; }
.frow .fld.grow { flex:1; width:auto; min-width:150px; }
.frow select, .frow input[type=text], .frow button {
height:40px; box-sizing:border-box; font-size:14px; line-height:1; margin:0;
}
.frow select { min-width:0; width:100%; padding:0 30px 0 12px; background-position:right 10px center; }
/* The ufw switch hangs under the button, centred on it, level with the note. */
.frow .act { position:relative; }
.frow .act label.check {
position:absolute; top:100%; left:50%; transform:translateX(-50%);
display:inline-flex; align-items:center; gap:8px; height:20px; margin:12px 0 0;
color:var(--text); font-size:14px; cursor:pointer;
}
.under { min-height:20px; margin:12px 110px 0 0; font-size:13px; line-height:20px; }
/* Phones: two fields a line, the label and the button each a line of their own. */
@media (max-width: 640px) {
.frow .fld, .frow .fld.addr { width:calc(50% - 6px); }
.frow .fld.grow { flex:1 1 100%; }
.frow .act { display:flex; align-items:center; gap:16px; width:100%; }
.frow .act button { flex:1; }
.frow .act label.check { position:static; transform:none; margin:0; }
.under { margin-right:0; }
}
</style>
</head>
<body>
<header class="topbar">
<div class="brand"><a class="home" href="/" title="К списку машин">LPRA</a> · Порты - {{.Agent.Name}} <span class="muted">({{.Agent.Hostname}})</span></div>
<nav>
<a href="/">← Машины</a>
<a href="/agents/{{.Agent.ID}}/terminal">Терминал</a>
<a href="/agents/{{.Agent.ID}}/files">Файлы</a>
<a href="/agents/{{.Agent.ID}}/status">Статус</a>
<a href="/agents/{{.Agent.ID}}/edit">Изменить</a>
</nav>
</header>
<div class="wrap">
<h1>Проброс портов</h1>
<p class="muted">Порт сервиса на машине-клиенте становится доступен на порту хоста.
Клиент за NAT - трафик идёт исходящим подключением агента.</p>
{{if .Error}}<div class="err">{{.Error}}</div>{{end}}
<div class="card">
<h2>Правила</h2>
{{if .Rules}}
<table class="stack">
<thead><tr><th>Тип</th><th>Хост (порт)</th><th>→ Клиент (порт)</th><th>Слушать на</th><th>Метка</th><th></th></tr></thead>
<tbody>
{{range .Rules}}
<tr>
<td data-l="Тип"><code>{{.Proto}}</code></td>
<td class="muted" data-l="Хост">:{{.HostPort}}</td>
<td class="muted" data-l="→ Клиент">127.0.0.1:{{.ClientPort}}</td>
<td class="muted" data-l="Слушать на">{{.ListenAddr}}{{if and .NoUfw $.UfwEnabled}} · без ufw{{end}}</td>
<td class="lead">{{.Label}}</td>
<td class="actions" style="text-align:right">
<form method="post" action="/forwards/{{.ID}}/delete" style="display:inline"
onsubmit="return confirm('Удалить правило?')">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<button class="btn danger small" type="submit">Удалить</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<p class="muted">Правил пока нет.</p>
{{end}}
</div>
<div class="card">
<h2>Новое правило</h2>
<form method="post" action="/agents/{{.Agent.ID}}/forwards/add">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<div class="frow">
<div class="fld">
<label>Тип</label>
<select name="proto" id="proto">
<option value="http">http-lp</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
<div class="fld">
<label>Порт клиента</label>
<input type="text" name="client_port" placeholder="22" required>
</div>
<div class="fld">
<label>Порт хоста</label>
<input type="text" name="host_port" placeholder="2222" required>
</div>
<div class="fld addr">
<label>Слушать на</label>
<input type="text" name="listen_addr" value="0.0.0.0">
</div>
<div class="fld grow">
<label>Метка</label>
<input type="text" name="label" maxlength="80" placeholder="необязательно">
</div>
<div class="act">
<button type="submit">Создать</button>
{{if .UfwEnabled}}
<label class="check" title="Порт не будет открыт в ufw, и при удалении правила в ufw ничего не изменится">
<input type="checkbox" name="no_ufw" value="1"> Без ufw
</label>
{{end}}
</div>
</div>
<div class="under"><span class="muted" id="note"></span></div>
</form>
</div>
<div class="card">
<h2>Режимы</h2>
<ul class="muted" style="font-size:14px; line-height:1.7">
<li><b>http-lp</b> - туннель поверх HTTP long-poll (через адрес панели, порт {{.PanelPort}}). Работает везде, где доступна панель, но задержка выше.</li>
<li><b>tcp</b> - прямой TCP-мультиплексор через порт <b>{{.TunnelPort}}</b> хоста. Минимальная задержка, keep-alive, TCP_NODELAY. Нужно открыть {{.TunnelPort}}/tcp на хосте.</li>
<li><b>udp</b> - прямой UDP через порт <b>{{.TunnelPort}}</b> хоста. Нужно открыть {{.TunnelPort}}/udp.</li>
</ul>
<p class="muted" style="font-size:13px">Для tcp/udp агент держит исходящее соединение к <code>host:{{.TunnelPort}}</code>; несколько клиентов и портов мультиплексируются на нём.</p>
</div>
</div>
<script>
var notes = {
http: 'Трафик идёт через порт панели {{.PanelPort}} (long-poll). Порт {{.TunnelPort}} не требуется.',
tcp: 'Требуется открытый порт {{.TunnelPort}}/tcp на хосте. Самая низкая задержка.',
udp: 'Требуется открытый порт {{.TunnelPort}}/udp на хосте.'
};
var sel = document.getElementById('proto'), note = document.getElementById('note');
function upd(){ note.textContent = notes[sel.value] || ''; }
sel.onchange = upd; upd();
</script>
</body>
</html>