148 lines
7.0 KiB
HTML
148 lines
7.0 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"}}">
|
|
<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">LPRA · Порты - {{.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>
|