Files
2026-09-07 02:45:48 +03:00

273 lines
7.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}