Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions connectctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -699,26 +699,26 @@ func sink(opts docopt.Opts) {
}

type Receive struct {
source connect.TransferPath
frames []*protocol.Frame
provideMode protocol.ProvideMode
source connect.TransferPath
frames []*protocol.Frame
peer connect.Peer
}

receives := make(chan *Receive)

client.AddReceiveCallback(func(source connect.TransferPath, frames []*protocol.Frame, provideMode protocol.ProvideMode) {
client.AddReceiveCallback(func(source connect.TransferPath, frames []*protocol.Frame, peer connect.Peer) {
receives <- &Receive{
source: source,
frames: frames,
provideMode: provideMode,
source: source,
frames: frames,
peer: peer,
}
})

// FIXME reassemble the chunks. Only a complete message counts as 1 against the message count
for i := 0; messageCount < 0 || i < messageCount; i += 1 {
select {
case receive := <-receives:
fmt.Printf("[%s %s] %s\n", receive.source, receive.provideMode, receive.frames)
fmt.Printf("[%s %s] %s\n", receive.source, receive.peer.ProvideMode, receive.frames)
}
}
}
83 changes: 83 additions & 0 deletions egress.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package connect

import (
"net"
"sync/atomic"
"syscall"
)

// Socket self-exclusion (R1 for the Windows VPN service).
//
// When this process itself provides a VPN tunnel (a DeviceLocal writing a tun
// adapter), its own outbound sockets must not route back into that tunnel, or
// the platform/provider connections loop. macOS network extensions and Android
// VpnService.protect solve this at the OS layer; Windows has no per-application
// exclusion, so connect binds its outbound sockets to the physical interface
// via IP_UNICAST_IF / IPV6_UNICAST_IF (the wireguard-windows pattern). A socket
// with a forced egress interface ignores the tun's default route.
//
// The binding is a no-op unless an egress index is set, and a no-op on every
// non-Windows platform (see egress_other.go), so this is inert for the mobile
// and desktop app builds.

var egressInterfaceIndex4 atomic.Uint32
var egressInterfaceIndex6 atomic.Uint32

// SetEgressInterfaceIndex binds connect's outbound sockets to the given
// physical interface indices (IPv4 and IPv6). Pass 0 for a family to leave that
// family unbound. Safe to call repeatedly, e.g. from a network-change handler.
// Only takes effect on Windows.
func SetEgressInterfaceIndex(index4 uint32, index6 uint32) {
egressInterfaceIndex4.Store(index4)
egressInterfaceIndex6.Store(index6)
}

// EgressInterfaceIndex returns the current (index4, index6). Zero means unset.
func EgressInterfaceIndex() (uint32, uint32) {
return egressInterfaceIndex4.Load(), egressInterfaceIndex6.Load()
}

// egressControl is a net.Dialer / net.ListenConfig Control callback that applies
// the egress interface binding to a socket at creation time.
func egressControl(_ string, _ string, c syscall.RawConn) error {
index4, index6 := EgressInterfaceIndex()
if index4 == 0 && index6 == 0 {
return nil
}
var innerErr error
err := c.Control(func(fd uintptr) {
innerErr = applyEgressInterface(fd, index4, index6)
})
if err != nil {
return err
}
return innerErr
}

// egressDialer installs the egress control on d, chaining any existing Control.
func egressDialer(d *net.Dialer) *net.Dialer {
prev := d.Control
d.Control = func(network string, address string, c syscall.RawConn) error {
if prev != nil {
if err := prev(network, address, c); err != nil {
return err
}
}
return egressControl(network, address, c)
}
return d
}

// applyEgress binds an already-created socket-backed conn (e.g. *net.UDPConn) to
// the egress interface. A no-op when no egress index is set or off Windows.
func applyEgress(conn syscall.Conn) error {
index4, index6 := EgressInterfaceIndex()
if index4 == 0 && index6 == 0 {
return nil
}
raw, err := conn.SyscallConn()
if err != nil {
return err
}
return egressControl("", "", raw)
}
50 changes: 50 additions & 0 deletions egress_net.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//go:build !js

package connect

import (
"net"
"syscall"

"github.com/pion/transport/v4"
"github.com/pion/transport/v4/stdnet"
)

// egressNet is a pion transport.Net that binds the UDP sockets pion creates for
// ICE to the egress interface, so p2p (webrtc) traffic does not loop back into
// the tunnel this process provides (R1). It embeds the standard net and only
// overrides socket creation; a no-op unless an egress index is set / off
// Windows.
type egressNet struct {
transport.Net
}

func newEgressNet() (transport.Net, error) {
base, err := stdnet.NewNet()
if err != nil {
return nil, err
}
return &egressNet{Net: base}, nil
}

func (self *egressNet) ListenUDP(network string, locAddr *net.UDPAddr) (transport.UDPConn, error) {
conn, err := self.Net.ListenUDP(network, locAddr)
if err != nil {
return nil, err
}
if sc, ok := conn.(syscall.Conn); ok {
_ = applyEgress(sc)
}
return conn, nil
}

func (self *egressNet) ListenPacket(network string, address string) (net.PacketConn, error) {
conn, err := self.Net.ListenPacket(network, address)
if err != nil {
return nil, err
}
if sc, ok := conn.(syscall.Conn); ok {
_ = applyEgress(sc)
}
return conn, nil
}
11 changes: 11 additions & 0 deletions egress_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build !windows

package connect

// On non-Windows platforms self-exclusion is handled at the OS layer: macOS
// network extensions bypass their own tunnel automatically, and Android uses
// VpnService.protect / addDisallowedApplication. The forced-egress binding is a
// no-op so the egress control is inert in the mobile and desktop app builds.
func applyEgressInterface(_ uintptr, _ uint32, _ uint32) error {
return nil
}
37 changes: 37 additions & 0 deletions egress_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//go:build windows

package connect

import (
"golang.org/x/sys/windows"
)

// IP_UNICAST_IF (IPPROTO_IP, option 31) forces the outgoing interface for
// unicast, overriding the route table. The interface index must be passed in
// NETWORK byte order. IPV6_UNICAST_IF (IPPROTO_IPV6, option 31) does the same
// for IPv6 but takes the index in HOST byte order. This asymmetry matches
// wireguard-windows' bindSocketRoute.
const (
ipUnicastIf = 31
ipv6UnicastIf = 31
)

func htonl(x uint32) uint32 {
return (x&0x000000ff)<<24 | (x&0x0000ff00)<<8 | (x&0x00ff0000)>>8 | (x&0xff000000)>>24
}

// applyEgressInterface sets the forced egress interface on the socket. Both
// families are set best-effort: the option for the wrong family on a
// single-family socket fails harmlessly, so per-option errors are ignored (as
// in wireguard-windows). This guarantees the call never breaks connection
// setup even when only one family applies.
func applyEgressInterface(fd uintptr, index4 uint32, index6 uint32) error {
h := windows.Handle(fd)
if index4 != 0 {
_ = windows.SetsockoptInt(h, windows.IPPROTO_IP, ipUnicastIf, int(htonl(index4)))
}
if index6 != 0 {
_ = windows.SetsockoptInt(h, windows.IPPROTO_IPV6, ipv6UnicastIf, int(index6))
}
return nil
}
Loading
Loading