mirror of
https://github.com/hackdiaz-dev/free-turn-proxy.git
synced 2026-10-12 09:41:53 +02:00
feat(auth)!: подтверждение Client ID с повтором по таймеру
This commit is contained in:
+21
-23
@@ -202,40 +202,38 @@ func handleAccepted(ctx context.Context, logger logx.Logger, db *clientsdb.DB, c
|
||||
}
|
||||
logger.Debugf("Handshake done")
|
||||
|
||||
// Wire-контракт: клиент всегда передаёт Client ID первой app-record.
|
||||
clientID, clientMode, err := clientsdb.ReadClientID(dtlsConn)
|
||||
clientID, data, err := clientsdb.AcceptClientID(dtlsConn, func(id string, mode byte) error {
|
||||
return admitClient(cfg, db, id, mode)
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warnf("Read Client ID failed: %v", err)
|
||||
logger.Warnf("Client ID from %s rejected: %v", conn.RemoteAddr(), err)
|
||||
return
|
||||
}
|
||||
if want := wireMode(cfg.Proxy.Mode); clientMode != clientsdb.ModeUnset && clientMode != want {
|
||||
logger.Warnf("Mode mismatch from %s: клиент %s, сервер %s - трафик не пойдёт, приведите -mode к одному значению",
|
||||
conn.RemoteAddr(), modeName(clientMode), cfg.Proxy.Mode)
|
||||
return
|
||||
}
|
||||
if clientMode == clientsdb.ModeUnset && cfg.Proxy.Mode == config.ProxyModeTCP {
|
||||
logger.Warnf("Mode mismatch from %s: клиент без тега режима (udp), сервер tcp", conn.RemoteAddr())
|
||||
return
|
||||
}
|
||||
if db != nil {
|
||||
if !db.IsAuthorized(clientID) {
|
||||
logger.Warnf("Unauthorized Client ID: %s. Dropping connection.", clientID)
|
||||
return
|
||||
}
|
||||
logger.Debugf("Client %s authorized", clientID)
|
||||
} else {
|
||||
logger.Debugf("Client ID received (no allowlist): %s", clientID)
|
||||
}
|
||||
|
||||
logger.Infof("Session up: client=%s from=%s", clientID, conn.RemoteAddr())
|
||||
if cfg.Proxy.Mode == config.ProxyModeTCP {
|
||||
tcpserver.Handle(ctx, logger, dtlsConn, cfg.Proxy.Connect, cfg.KCP.Profile)
|
||||
tcpserver.Handle(ctx, logger, data, cfg.Proxy.Connect, cfg.KCP.Profile)
|
||||
} else {
|
||||
udpserver.Handle(ctx, logger, conn, cfg.Proxy.Connect)
|
||||
udpserver.Handle(ctx, logger, data, cfg.Proxy.Connect)
|
||||
}
|
||||
logger.Infof("Session down: client=%s from=%s", clientID, conn.RemoteAddr())
|
||||
}
|
||||
|
||||
// admitClient: режим клиента обязан совпасть с сервером, ID - быть в allowlist (если он есть).
|
||||
func admitClient(cfg *config.Server, db *clientsdb.DB, id string, mode byte) error {
|
||||
if want := wireMode(cfg.Proxy.Mode); mode != clientsdb.ModeUnset && mode != want {
|
||||
return fmt.Errorf("mode mismatch: клиент %s, сервер %s - приведите -mode к одному значению",
|
||||
modeName(mode), cfg.Proxy.Mode)
|
||||
}
|
||||
if mode == clientsdb.ModeUnset && cfg.Proxy.Mode == config.ProxyModeTCP {
|
||||
return errors.New("mode mismatch: клиент без тега режима (udp), сервер tcp")
|
||||
}
|
||||
if db != nil && !db.IsAuthorized(id) {
|
||||
return fmt.Errorf("unauthorized client ID %s", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleClientsCommand(args []string) {
|
||||
if len(args) == 0 {
|
||||
fmt.Println("Usage: server clients <add|remove|list> [args...]")
|
||||
|
||||
+98
-21
@@ -1,8 +1,10 @@
|
||||
package clientsdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
@@ -187,50 +189,125 @@ func syncDir(dir string) {
|
||||
_ = d.Close()
|
||||
}
|
||||
|
||||
// Тег режима едет хвостом той же записи: клиент до этого поля его не писал, а читатель
|
||||
// брал ровно 1+len байт - лишний байт старый сервер молча пропускает.
|
||||
const (
|
||||
ModeUnset byte = 0
|
||||
ModeUDP byte = 1
|
||||
ModeTCP byte = 2
|
||||
|
||||
// idVersionAck - клиент ждёт подтверждения ID.
|
||||
idVersionAck byte = 2
|
||||
idAck byte = 0x06
|
||||
|
||||
idReadTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// WriteClientID отправляет Client ID (1 байт длины + строка + 1 байт режима).
|
||||
func WriteClientID(conn net.Conn, clientID string, mode byte) error {
|
||||
// ErrNoIDAck - сервер не подтвердил Client ID: он старше протокола подтверждения или
|
||||
// канал теряет всё подряд.
|
||||
var ErrNoIDAck = errors.New("clientsdb: server did not acknowledge client ID (server outdated?)")
|
||||
|
||||
// idRetransmit - таймеры повтора ID (RFC 6347 §4.2.4: старт 1 с, удвоение).
|
||||
var idRetransmit = []time.Duration{time.Second, 2 * time.Second, 4 * time.Second}
|
||||
|
||||
func idRecord(clientID string, mode byte) []byte {
|
||||
b := []byte(clientID)
|
||||
if len(b) > 255 {
|
||||
b = b[:255]
|
||||
}
|
||||
buf := make([]byte, 1+len(b)+1)
|
||||
buf[0] = byte(len(b)) //nolint:gosec // len(b) усечён до ≤255 выше
|
||||
copy(buf[1:], b)
|
||||
buf[1+len(b)] = mode
|
||||
_, err := conn.Write(buf)
|
||||
return err
|
||||
buf := make([]byte, 0, len(b)+3)
|
||||
buf = append(buf, byte(len(b))) //nolint:gosec // len(b) усечён до ≤255 выше
|
||||
buf = append(buf, b...)
|
||||
return append(buf, mode, idVersionAck)
|
||||
}
|
||||
|
||||
// ReadClientID читает Client ID из первой DTLS-записи. Режим ModeUnset - клиент старше
|
||||
// тега, режим у него всегда udp.
|
||||
func ReadClientID(conn net.Conn) (string, byte, error) {
|
||||
_ = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
func WriteClientID(ctx context.Context, conn net.Conn, clientID string, mode byte) error {
|
||||
defer func() { _ = conn.SetReadDeadline(time.Time{}) }()
|
||||
rec := idRecord(clientID, mode)
|
||||
var buf [16]byte
|
||||
for _, wait := range idRetransmit {
|
||||
if _, err := conn.Write(rec); err != nil {
|
||||
return fmt.Errorf("send client ID: %w", err)
|
||||
}
|
||||
if err := conn.SetReadDeadline(time.Now().Add(wait)); err != nil {
|
||||
return fmt.Errorf("client ID deadline: %w", err)
|
||||
}
|
||||
for {
|
||||
n, err := conn.Read(buf[:])
|
||||
if err == nil && n == 1 && buf[0] == idAck {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
var ne net.Error
|
||||
if !errors.As(err, &ne) || !ne.Timeout() {
|
||||
return fmt.Errorf("await client ID ack: %w", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
return ErrNoIDAck
|
||||
}
|
||||
|
||||
func readClientID(conn net.Conn) (string, byte, byte, error) {
|
||||
_ = conn.SetReadDeadline(time.Now().Add(idReadTimeout))
|
||||
defer func() { _ = conn.SetReadDeadline(time.Time{}) }()
|
||||
|
||||
buf := make([]byte, 257)
|
||||
buf := make([]byte, 258)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
return "", ModeUnset, err
|
||||
return "", ModeUnset, 0, err
|
||||
}
|
||||
if n == 0 {
|
||||
return "", ModeUnset, nil
|
||||
return "", ModeUnset, 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
l := int(buf[0])
|
||||
if n < 1+l {
|
||||
return "", ModeUnset, io.ErrUnexpectedEOF
|
||||
return "", ModeUnset, 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
mode := ModeUnset
|
||||
var mode, ver byte
|
||||
if n > 1+l {
|
||||
mode = buf[1+l]
|
||||
}
|
||||
return string(buf[1 : 1+l]), mode, nil
|
||||
if n > 2+l {
|
||||
ver = buf[2+l]
|
||||
}
|
||||
return string(buf[1 : 1+l]), mode, ver, nil
|
||||
}
|
||||
|
||||
func AcceptClientID(conn net.Conn, authorize func(id string, mode byte) error) (string, net.Conn, error) {
|
||||
id, mode, ver, err := readClientID(conn)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if err := authorize(id, mode); err != nil {
|
||||
return id, nil, err
|
||||
}
|
||||
if ver < idVersionAck {
|
||||
return id, conn, nil
|
||||
}
|
||||
if _, err := conn.Write([]byte{idAck}); err != nil {
|
||||
return id, nil, fmt.Errorf("ack client ID: %w", err)
|
||||
}
|
||||
return id, &ackedConn{Conn: conn, rec: idRecord(id, mode)}, nil
|
||||
}
|
||||
|
||||
type ackedConn struct {
|
||||
net.Conn
|
||||
rec []byte
|
||||
data bool
|
||||
}
|
||||
|
||||
func (c *ackedConn) Read(b []byte) (int, error) {
|
||||
for {
|
||||
n, err := c.Conn.Read(b)
|
||||
if err != nil || c.data || !bytes.Equal(b[:n], c.rec) {
|
||||
c.data = c.data || err == nil
|
||||
return n, err
|
||||
}
|
||||
if _, err := c.Write([]byte{idAck}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+124
-44
@@ -1,11 +1,14 @@
|
||||
package clientsdb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cbeuw/connutil"
|
||||
)
|
||||
|
||||
func TestFailedSavePreservesAuthorization(t *testing.T) {
|
||||
@@ -103,65 +106,142 @@ func TestClientsDB(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// dropWrites теряет первые n записей - модель потерь датаграмм.
|
||||
type dropWrites struct {
|
||||
net.Conn
|
||||
n int
|
||||
}
|
||||
|
||||
func (d *dropWrites) Write(b []byte) (int, error) {
|
||||
if d.n > 0 {
|
||||
d.n--
|
||||
return len(b), nil
|
||||
}
|
||||
return d.Conn.Write(b)
|
||||
}
|
||||
|
||||
const testID = "e8030ba252344e536664d96f4544d64d"
|
||||
|
||||
func accept(t *testing.T, conn net.Conn) (string, byte, net.Conn, error) {
|
||||
t.Helper()
|
||||
var gotMode byte
|
||||
id, data, err := AcceptClientID(conn, func(_ string, mode byte) error {
|
||||
gotMode = mode
|
||||
return nil
|
||||
})
|
||||
return id, gotMode, data, err
|
||||
}
|
||||
|
||||
func TestClientIDRoundTrip(t *testing.T) {
|
||||
addr := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}
|
||||
serverConn, err := net.ListenUDP("udp", addr)
|
||||
if err != nil {
|
||||
t.Fatalf("ListenUDP: %v", err)
|
||||
}
|
||||
defer func() { _ = serverConn.Close() }()
|
||||
client, server := pipe()
|
||||
errc := make(chan error, 1)
|
||||
go func() { errc <- WriteClientID(t.Context(), client, testID, ModeTCP) }()
|
||||
|
||||
udpAddr, ok := serverConn.LocalAddr().(*net.UDPAddr)
|
||||
if !ok {
|
||||
t.Fatalf("LocalAddr is not *net.UDPAddr")
|
||||
id, mode, _, err := accept(t, server)
|
||||
if err != nil || id != testID || mode != ModeTCP {
|
||||
t.Fatalf("accept = %q/%d/%v, want %q/%d", id, mode, err, testID, ModeTCP)
|
||||
}
|
||||
clientConn, err := net.DialUDP("udp", nil, udpAddr)
|
||||
if err != nil {
|
||||
t.Fatalf("DialUDP: %v", err)
|
||||
if err := <-errc; err != nil {
|
||||
t.Fatalf("WriteClientID: %v", err)
|
||||
}
|
||||
defer func() { _ = clientConn.Close() }()
|
||||
}
|
||||
|
||||
expectedID := "client-test-uuid-123"
|
||||
// Потеря первой ID-записи: клиент повторяет по таймеру, сессия поднимается.
|
||||
func TestClientIDSurvivesLostRecord(t *testing.T) {
|
||||
client, server := pipe()
|
||||
errc := make(chan error, 1)
|
||||
go func() { errc <- WriteClientID(t.Context(), &dropWrites{Conn: client, n: 1}, testID, ModeUDP) }()
|
||||
|
||||
if id, _, _, err := accept(t, server); err != nil || id != testID {
|
||||
t.Fatalf("accept = %q/%v", id, err)
|
||||
}
|
||||
if err := <-errc; err != nil {
|
||||
t.Fatalf("WriteClientID: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientIDSurvivesLostAck(t *testing.T) {
|
||||
client, server := pipe()
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
if werr := WriteClientID(clientConn, expectedID, ModeTCP); werr != nil {
|
||||
t.Errorf("WriteClientID failed: %v", werr)
|
||||
if err := WriteClientID(t.Context(), client, testID, ModeUDP); err != nil {
|
||||
errc <- err
|
||||
return
|
||||
}
|
||||
_, err := client.Write([]byte("payload"))
|
||||
errc <- err
|
||||
}()
|
||||
|
||||
readID, mode, err := ReadClientID(serverConn)
|
||||
_, _, data, err := accept(t, &dropWrites{Conn: server, n: 1})
|
||||
if err != nil {
|
||||
t.Fatalf("ReadClientID: %v", err)
|
||||
t.Fatalf("accept: %v", err)
|
||||
}
|
||||
|
||||
if readID != expectedID {
|
||||
t.Errorf("expected %q, got %q", expectedID, readID)
|
||||
buf := make([]byte, 64)
|
||||
n, err := data.Read(buf)
|
||||
if err != nil || string(buf[:n]) != "payload" {
|
||||
t.Fatalf("data = %q/%v, want payload", buf[:n], err)
|
||||
}
|
||||
if mode != ModeTCP {
|
||||
t.Errorf("mode = %d, want %d", mode, ModeTCP)
|
||||
if err := <-errc; err != nil {
|
||||
t.Fatalf("client: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Клиент старше тега режима слал ровно 1+len байт - такую запись читатель обязан принять.
|
||||
func TestReadClientIDLegacyRecordHasNoMode(t *testing.T) {
|
||||
clientConn, serverConn := net.Pipe()
|
||||
defer func() { _ = clientConn.Close() }()
|
||||
defer func() { _ = serverConn.Close() }()
|
||||
|
||||
const id = "legacy-client"
|
||||
go func() {
|
||||
buf := append([]byte{byte(len(id))}, id...) //nolint:gosec // длина константы заведомо ≤255
|
||||
_, _ = clientConn.Write(buf)
|
||||
}()
|
||||
|
||||
readID, mode, err := ReadClientID(serverConn)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadClientID: %v", err)
|
||||
}
|
||||
if readID != id {
|
||||
t.Errorf("id = %q, want %q", readID, id)
|
||||
}
|
||||
if mode != ModeUnset {
|
||||
t.Errorf("mode = %d, want ModeUnset", mode)
|
||||
// Клиенты старше подтверждения: без режима ([len][id]) и с режимом ([len][id][mode]).
|
||||
// Им сервер не отвечает и отдаёт соединение как есть - поведение до подтверждения.
|
||||
func TestAcceptLegacyRecordNoAck(t *testing.T) {
|
||||
head := append([]byte{byte(len(testID))}, testID...)
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
rec []byte
|
||||
mode byte
|
||||
}{
|
||||
{"no mode", head, ModeUnset},
|
||||
{"with mode", append(append([]byte{}, head...), ModeUDP), ModeUDP},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
client, server := pipe()
|
||||
if _, err := client.Write(tc.rec); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id, mode, data, err := accept(t, server)
|
||||
if err != nil || id != testID || mode != tc.mode || data != server {
|
||||
t.Fatalf("accept = %q/%d/%v, conn untouched=%v", id, mode, err, data == server)
|
||||
}
|
||||
_ = client.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
||||
if n, err := client.Read(make([]byte, 4)); err == nil {
|
||||
t.Fatalf("legacy client got %d bytes of ack", n)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceptRejectsWithoutAck(t *testing.T) {
|
||||
client, server := pipe()
|
||||
if _, err := client.Write(idRecord(testID, ModeUDP)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
denied := errors.New("denied")
|
||||
if _, _, err := AcceptClientID(server, func(string, byte) error { return denied }); !errors.Is(err, denied) {
|
||||
t.Fatalf("err = %v, want denied", err)
|
||||
}
|
||||
_ = client.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
||||
if n, err := client.Read(make([]byte, 4)); err == nil {
|
||||
t.Fatalf("rejected client got %d bytes of ack", n)
|
||||
}
|
||||
}
|
||||
|
||||
// pipe - датаграммная пара; таймаут отдаётся как у DTLS (net.Error), а не своей ошибкой connutil.
|
||||
func pipe() (net.Conn, net.Conn) {
|
||||
a, b := connutil.AsyncPacketPipe()
|
||||
return &netTimeout{a}, &netTimeout{b}
|
||||
}
|
||||
|
||||
type netTimeout struct{ net.Conn }
|
||||
|
||||
func (c *netTimeout) Read(b []byte) (int, error) {
|
||||
n, err := c.Conn.Read(b)
|
||||
if errors.Is(err, connutil.ErrTimeout) {
|
||||
err = os.ErrDeadlineExceeded
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
@@ -394,7 +394,7 @@ func createSession(ctx context.Context, deps *Deps, params *Params, peer *net.UD
|
||||
closers = append(closers, func() { _ = dtlsConn.Close() })
|
||||
|
||||
// Wire-контракт: Client ID первой app-record, до KCP.
|
||||
if err = clientsdb.WriteClientID(dtlsConn, params.ClientID, clientsdb.ModeTCP); err != nil {
|
||||
if err = clientsdb.WriteClientID(ctx, dtlsConn, params.ClientID, clientsdb.ModeTCP); err != nil {
|
||||
cleanup()
|
||||
return nil, fmt.Errorf("send client ID: %w", err)
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ func dtlsSession(dtlsctx context.Context, dtlscancel context.CancelFunc, deps *D
|
||||
}()
|
||||
deps.log().Debugf("[STREAM %d] Established DTLS connection", streamID)
|
||||
|
||||
if err := clientsdb.WriteClientID(dtlsConn, params.ClientID, clientsdb.ModeUDP); err != nil {
|
||||
if err := clientsdb.WriteClientID(dtlsctx, dtlsConn, params.ClientID, clientsdb.ModeUDP); err != nil {
|
||||
return fmt.Errorf("failed to write client ID: %w", err)
|
||||
}
|
||||
// Аллокация без DTLS ещё не может передавать пользовательский трафик.
|
||||
|
||||
Reference in New Issue
Block a user