mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 07:16:38 +00:00
The Relayed connection setup is optimistic. It does not have any confirmation of an established end-to-end connection. Peers start sending WireGuard handshake packets immediately after the successful offer-answer handshake. Meanwhile, for successful P2P connection negotiation, we change the WireGuard endpoint address, but this change does not trigger new handshake initiation. Because the peer switched from Relayed connection to P2P, the packets from the Relay server are dropped and must wait for the next WireGuard handshake via P2P. To avoid this scenario, the relayed WireGuard proxy no longer drops the packets. Instead, it rewrites the source address to the new P2P endpoint and continues forwarding the packets. We still have one corner case: if the Relayed server negotiation chooses a server that has not been used before. In this case, one side of the peer connection will be slower to reach the Relay server, and the Relay server will drop the handshake packet. If everything goes well we should see exactly 5 seconds improvements between the WireGuard configuration time and the handshake time.
218 lines
4.8 KiB
Go
218 lines
4.8 KiB
Go
package bind
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"strings"
|
|
"sync"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/client/iface/bind"
|
|
"github.com/netbirdio/netbird/client/iface/bufsize"
|
|
"github.com/netbirdio/netbird/client/iface/wgproxy/listener"
|
|
)
|
|
|
|
type IceBind interface {
|
|
SetEndpoint(fakeIP netip.Addr, conn net.Conn)
|
|
RemoveEndpoint(fakeIP netip.Addr)
|
|
Recv(ctx context.Context, msg bind.RecvMessage)
|
|
MTU() uint16
|
|
}
|
|
|
|
type ProxyBind struct {
|
|
bind IceBind
|
|
|
|
// wgRelayedEndpoint is a fake address that generated by the Bind.SetEndpoint based on the remote NetBird peer address
|
|
wgRelayedEndpoint *bind.Endpoint
|
|
wgCurrentUsed *bind.Endpoint
|
|
remoteConn net.Conn
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
closeMu sync.Mutex
|
|
closed bool
|
|
|
|
paused bool
|
|
pausedCond *sync.Cond
|
|
isStarted bool
|
|
|
|
closeListener *listener.CloseListener
|
|
}
|
|
|
|
func NewProxyBind(bind IceBind) *ProxyBind {
|
|
p := &ProxyBind{
|
|
bind: bind,
|
|
closeListener: listener.NewCloseListener(),
|
|
pausedCond: sync.NewCond(&sync.Mutex{}),
|
|
}
|
|
|
|
return p
|
|
}
|
|
|
|
// AddTurnConn adds a new connection to the bind.
|
|
// endpoint is the NetBird address of the remote peer. The SetEndpoint return with the address what will be used in the
|
|
// WireGuard configuration.
|
|
//
|
|
// Parameters:
|
|
// - ctx: Context is used for proxyToLocal to avoid unnecessary error messages
|
|
// - nbAddr: The NetBird UDP address of the remote peer, it required to generate fake address
|
|
// - remoteConn: The established TURN connection to the remote peer
|
|
func (p *ProxyBind) AddTurnConn(ctx context.Context, nbAddr *net.UDPAddr, remoteConn net.Conn) error {
|
|
fakeNetIP, err := fakeAddress(nbAddr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.wgRelayedEndpoint = &bind.Endpoint{AddrPort: *fakeNetIP}
|
|
p.remoteConn = remoteConn
|
|
p.ctx, p.cancel = context.WithCancel(ctx)
|
|
return nil
|
|
|
|
}
|
|
|
|
func (p *ProxyBind) EndpointAddr() *net.UDPAddr {
|
|
return bind.EndpointToUDPAddr(*p.wgRelayedEndpoint)
|
|
}
|
|
|
|
func (p *ProxyBind) SetDisconnectListener(disconnected func()) {
|
|
p.closeListener.SetCloseListener(disconnected)
|
|
}
|
|
|
|
func (p *ProxyBind) Work() {
|
|
if p.remoteConn == nil {
|
|
return
|
|
}
|
|
|
|
p.bind.SetEndpoint(p.wgRelayedEndpoint.Addr(), p.remoteConn)
|
|
|
|
p.pausedCond.L.Lock()
|
|
p.paused = false
|
|
|
|
p.wgCurrentUsed = p.wgRelayedEndpoint
|
|
|
|
// Start the proxy only once
|
|
if !p.isStarted {
|
|
p.isStarted = true
|
|
go p.proxyToLocal(p.ctx)
|
|
}
|
|
|
|
p.pausedCond.Signal()
|
|
p.pausedCond.L.Unlock()
|
|
}
|
|
|
|
func (p *ProxyBind) Pause() {
|
|
if p.remoteConn == nil {
|
|
return
|
|
}
|
|
|
|
p.pausedCond.L.Lock()
|
|
p.paused = true
|
|
p.pausedCond.L.Unlock()
|
|
}
|
|
|
|
func (p *ProxyBind) RedirectAs(endpoint *net.UDPAddr) {
|
|
p.pausedCond.L.Lock()
|
|
p.paused = false
|
|
|
|
p.wgCurrentUsed = addrToEndpoint(endpoint)
|
|
|
|
p.pausedCond.Signal()
|
|
p.pausedCond.L.Unlock()
|
|
}
|
|
|
|
func addrToEndpoint(addr *net.UDPAddr) *bind.Endpoint {
|
|
ip, _ := netip.AddrFromSlice(addr.IP.To4())
|
|
addrPort := netip.AddrPortFrom(ip, uint16(addr.Port))
|
|
return &bind.Endpoint{AddrPort: addrPort}
|
|
}
|
|
|
|
func (p *ProxyBind) CloseConn() error {
|
|
if p.cancel == nil {
|
|
return fmt.Errorf("proxy not started")
|
|
}
|
|
return p.close()
|
|
}
|
|
|
|
func (p *ProxyBind) close() error {
|
|
if p.remoteConn == nil {
|
|
return nil
|
|
}
|
|
|
|
p.closeMu.Lock()
|
|
defer p.closeMu.Unlock()
|
|
|
|
if p.closed {
|
|
return nil
|
|
}
|
|
|
|
p.closeListener.SetCloseListener(nil)
|
|
|
|
p.closed = true
|
|
|
|
p.cancel()
|
|
|
|
p.pausedCond.L.Lock()
|
|
p.paused = false
|
|
p.pausedCond.Signal()
|
|
p.pausedCond.L.Unlock()
|
|
|
|
p.bind.RemoveEndpoint(p.wgRelayedEndpoint.Addr())
|
|
|
|
if rErr := p.remoteConn.Close(); rErr != nil && !errors.Is(rErr, net.ErrClosed) {
|
|
return rErr
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *ProxyBind) proxyToLocal(ctx context.Context) {
|
|
defer func() {
|
|
if err := p.close(); err != nil {
|
|
log.Warnf("failed to close remote conn: %s", err)
|
|
}
|
|
}()
|
|
|
|
for {
|
|
buf := make([]byte, p.bind.MTU()+bufsize.WGBufferOverhead)
|
|
n, err := p.remoteConn.Read(buf)
|
|
if err != nil {
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
p.closeListener.Notify()
|
|
log.Errorf("failed to read from remote conn: %s, %s", p.remoteConn.RemoteAddr(), err)
|
|
return
|
|
}
|
|
|
|
p.pausedCond.L.Lock()
|
|
for p.paused {
|
|
p.pausedCond.Wait()
|
|
}
|
|
|
|
msg := bind.RecvMessage{
|
|
Endpoint: p.wgCurrentUsed,
|
|
Buffer: buf[:n],
|
|
}
|
|
p.bind.Recv(ctx, msg)
|
|
p.pausedCond.L.Unlock()
|
|
}
|
|
}
|
|
|
|
// fakeAddress returns a fake address that is used to as an identifier for the peer.
|
|
// The fake address is in the format of 127.1.x.x where x.x is the last two octets of the peer address.
|
|
func fakeAddress(peerAddress *net.UDPAddr) (*netip.AddrPort, error) {
|
|
octets := strings.Split(peerAddress.IP.String(), ".")
|
|
if len(octets) != 4 {
|
|
return nil, fmt.Errorf("invalid IP format")
|
|
}
|
|
|
|
fakeIP, err := netip.ParseAddr(fmt.Sprintf("127.1.%s.%s", octets[2], octets[3]))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse new IP: %w", err)
|
|
}
|
|
|
|
netipAddr := netip.AddrPortFrom(fakeIP, uint16(peerAddress.Port))
|
|
return &netipAddr, nil
|
|
}
|