mirror of
https://github.com/hackdiaz-dev/free-turn-proxy.git
synced 2026-10-12 09:41:53 +02:00
273 lines
7.4 KiB
Go
273 lines
7.4 KiB
Go
package mobile
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"math"
|
||
"net"
|
||
"net/netip"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/samosvalishe/free-turn-proxy/internal/config"
|
||
"github.com/samosvalishe/free-turn-proxy/internal/logx"
|
||
"github.com/samosvalishe/free-turn-proxy/internal/netconn"
|
||
"github.com/samosvalishe/free-turn-proxy/internal/tunnel"
|
||
"github.com/samosvalishe/free-turn-proxy/internal/tunnel/awg"
|
||
"github.com/samosvalishe/free-turn-proxy/internal/tunnel/bind"
|
||
"github.com/samosvalishe/free-turn-proxy/internal/tunnel/wgconf"
|
||
)
|
||
|
||
// TunnelSnapshot - срез состояния userspace-туннеля.
|
||
type TunnelSnapshot struct {
|
||
Up bool
|
||
RxBytes int64
|
||
TxBytes int64
|
||
// HandshakeAgeSec - секунд с последнего handshake (-1 если не было).
|
||
HandshakeAgeSec int64
|
||
}
|
||
|
||
func TunnelStats() *TunnelSnapshot {
|
||
l := current.Load()
|
||
if l == nil || l.tunnel == nil {
|
||
return &TunnelSnapshot{HandshakeAgeSec: -1}
|
||
}
|
||
st, err := l.tunnel.backend.Stats()
|
||
if err != nil {
|
||
return &TunnelSnapshot{Up: true, HandshakeAgeSec: -1}
|
||
}
|
||
age := int64(-1)
|
||
if !st.LastHandshake.IsZero() {
|
||
age = int64(time.Since(st.LastHandshake).Seconds())
|
||
}
|
||
return &TunnelSnapshot{
|
||
Up: true,
|
||
RxBytes: st.RxBytes,
|
||
TxBytes: st.TxBytes,
|
||
HandshakeAgeSec: age,
|
||
}
|
||
}
|
||
|
||
// TunnelParams - параметры tun-интерфейса для платформы (VpnService.Builder, NEPacketTunnelProvider).
|
||
type TunnelParams struct {
|
||
Addresses string
|
||
DNS string
|
||
AllowedIPs string
|
||
MTU int
|
||
}
|
||
|
||
// ParseTunnelConfig извлекает параметры tun-интерфейса для платформы из wg-конфига.
|
||
func ParseTunnelConfig(wgText string, mtu int) (*TunnelParams, error) {
|
||
cfg, err := wgconf.Parse(wgText)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("parse tunnel config: %w", err)
|
||
}
|
||
if mtu > 0 {
|
||
cfg.MTU = mtu
|
||
}
|
||
|
||
addrs := make([]string, 0, len(cfg.Addresses))
|
||
for _, p := range cfg.Addresses {
|
||
addrs = append(addrs, p.String())
|
||
}
|
||
dns := make([]string, 0, len(cfg.DNS))
|
||
for _, a := range cfg.DNS {
|
||
dns = append(dns, a.String())
|
||
}
|
||
return &TunnelParams{
|
||
Addresses: strings.Join(addrs, ","),
|
||
DNS: strings.Join(dns, ","),
|
||
AllowedIPs: strings.Join(allowedIPs(cfg.Peers), ","),
|
||
MTU: cfg.MTU,
|
||
}, nil
|
||
}
|
||
|
||
// allowedIPs исключает дубликаты префиксов (addRoute бросает исключение на дублях).
|
||
func allowedIPs(peers []tunnel.Peer) []string {
|
||
out := make([]string, 0, len(peers))
|
||
seen := make(map[netip.Prefix]struct{}, len(peers))
|
||
for i := range peers {
|
||
for _, p := range peers[i].AllowedIPs {
|
||
if _, dup := seen[p]; dup {
|
||
continue
|
||
}
|
||
seen[p] = struct{}{}
|
||
out = append(out, p.String())
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
type tunnelParts struct {
|
||
backend tunnel.Backend
|
||
bind *bind.SinglePeerBind
|
||
deviceSide net.PacketConn
|
||
relaySide net.PacketConn
|
||
}
|
||
|
||
// buildTunnel собирает userspace WireGuard туннель через in-memory пайп к сессии релея.
|
||
func buildTunnel(cfg *config.Client, logger logx.Logger) (*tunnelParts, *tunnel.Config, error) {
|
||
tunCfg, err := wgconf.Parse(cfg.Tunnel.Config)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
if cfg.Tunnel.MTU > 0 {
|
||
tunCfg.MTU = cfg.Tunnel.MTU
|
||
}
|
||
// При режиме wg маскировка отключается независимо от опций в конфиге.
|
||
if cfg.Tunnel.Mode == tunnel.ModeWG && tunCfg.Amnezia.Enabled() {
|
||
logger.Warnf("tunnel: mode=wg, параметры AmneziaWG из конфига игнорируются")
|
||
tunCfg.Amnezia = tunnel.AmneziaParams{}
|
||
}
|
||
for i := range tunCfg.Peers {
|
||
tunCfg.Peers[i].Endpoint = ""
|
||
}
|
||
|
||
deviceSide, relaySide := netconn.PacketPipe(tunCfg.MTU+tunnelPipeHeadroom, 0)
|
||
parts := &tunnelParts{
|
||
bind: bind.NewSinglePeerBind(deviceSide),
|
||
deviceSide: deviceSide,
|
||
relaySide: relaySide,
|
||
}
|
||
parts.backend = awg.New(awg.Deps{Bind: parts.bind, Log: logger})
|
||
return parts, tunCfg, nil
|
||
}
|
||
|
||
// tunnelPipeHeadroom - запас MTU под оверхед заголовков WireGuard и junk-префиксы AmneziaWG.
|
||
const tunnelPipeHeadroom = 512
|
||
|
||
// Bind закрывается первым для разблокировки читающей горутины device.Close().
|
||
func (t *tunnelParts) close() {
|
||
if t == nil {
|
||
return
|
||
}
|
||
if t.bind != nil {
|
||
_ = t.bind.Close()
|
||
}
|
||
if t.backend != nil {
|
||
_ = t.backend.Down()
|
||
}
|
||
if t.deviceSide != nil {
|
||
_ = t.deviceSide.Close()
|
||
}
|
||
if t.relaySide != nil {
|
||
_ = t.relaySide.Close()
|
||
}
|
||
}
|
||
|
||
// StartTunnel запускает сессию с переданным tun FD (хост передаёт дубликат fd).
|
||
func StartTunnel(configJSON string, tunFD int) error {
|
||
if err := checkTunFD(tunFD); err != nil {
|
||
return err
|
||
}
|
||
mu.Lock()
|
||
defer mu.Unlock()
|
||
if current.Load() != nil {
|
||
awg.CloseTUNFD(tunFD)
|
||
return errors.New("already running")
|
||
}
|
||
return startLocked(configJSON, tunFD, true)
|
||
}
|
||
|
||
// checkTunFD валидирует FD до передачи ядру (0 - stdin процесса).
|
||
func checkTunFD(fd int) error {
|
||
if fd <= 0 {
|
||
return fmt.Errorf("bad tun fd %d", fd)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
const directHandshakeStale = 180 * time.Second
|
||
|
||
func StartDirectTunnel(wgText string, mtu int, tunFD int) error {
|
||
if err := checkTunFD(tunFD); err != nil {
|
||
return err
|
||
}
|
||
mu.Lock()
|
||
defer mu.Unlock()
|
||
if current.Load() != nil {
|
||
awg.CloseTUNFD(tunFD)
|
||
return errors.New("already running")
|
||
}
|
||
|
||
cfg, err := directConfig(wgText, mtu)
|
||
if err != nil {
|
||
awg.CloseTUNFD(tunFD)
|
||
return err
|
||
}
|
||
|
||
if protector.Load() == nil {
|
||
coreLog().Warnf("tunnel: SetProtect не задан - сокеты прямого туннеля не защищены")
|
||
}
|
||
|
||
backend := awg.New(awg.Deps{Log: coreLog(), Protect: protectFD})
|
||
|
||
if err := backend.Up(cfg, tunFD); err != nil {
|
||
return fmt.Errorf("start direct tunnel: %w", err)
|
||
}
|
||
|
||
done := make(chan struct{})
|
||
close(done)
|
||
current.Store(&live{
|
||
cancel: func() {},
|
||
done: done,
|
||
tunnel: &tunnelParts{backend: backend},
|
||
})
|
||
finishing.Store(nil)
|
||
lastStop.Store(nil)
|
||
return nil
|
||
}
|
||
|
||
func directConfig(wgText string, mtu int) (*tunnel.Config, error) {
|
||
cfg, err := wgconf.Parse(wgText)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("parse tunnel config: %w", err)
|
||
}
|
||
if mtu > 0 {
|
||
cfg.MTU = mtu
|
||
}
|
||
for i := range cfg.Peers {
|
||
if _, err := netip.ParseAddrPort(cfg.Peers[i].Endpoint); err != nil {
|
||
return nil, fmt.Errorf("peer[%d]: Endpoint must be ip:port for a direct tunnel, got %q", i, cfg.Peers[i].Endpoint)
|
||
}
|
||
}
|
||
return cfg, nil
|
||
}
|
||
|
||
func directSnapshot(l *live) *Snapshot {
|
||
st, err := l.tunnel.backend.Stats()
|
||
if err != nil {
|
||
return &Snapshot{State: StateError, ErrMsg: err.Error(), Streams: 1, Total: 1}
|
||
}
|
||
snap := &Snapshot{
|
||
State: StateConnecting,
|
||
Streams: 1,
|
||
Total: 1,
|
||
TxTotal: st.TxBytes,
|
||
RxTotal: st.RxBytes,
|
||
}
|
||
if !st.LastHandshake.IsZero() {
|
||
snap.State = StateConnected
|
||
}
|
||
l.fillRates(snap)
|
||
if snap.State == StateConnected && snap.TxRate > 0 && time.Since(st.LastHandshake) > directHandshakeStale {
|
||
snap.State = StateConnecting
|
||
}
|
||
return snap
|
||
}
|
||
|
||
func (l *live) fillRates(s *Snapshot) {
|
||
l.fillRatesAt(s, time.Now())
|
||
}
|
||
|
||
func (l *live) fillRatesAt(s *Snapshot, now time.Time) {
|
||
l.rateMu.Lock()
|
||
defer l.rateMu.Unlock()
|
||
|
||
if dt := now.Sub(l.prevAt).Seconds(); !l.prevAt.IsZero() && dt > 0 {
|
||
s.TxRate = int64(math.Round(float64(s.TxTotal-l.prevTx) / dt))
|
||
s.RxRate = int64(math.Round(float64(s.RxTotal-l.prevRx) / dt))
|
||
}
|
||
l.prevTx, l.prevRx, l.prevAt = s.TxTotal, s.RxTotal, now
|
||
}
|