Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
18 changes: 9 additions & 9 deletions client/go/outline/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
"localhost/client/go/outline/configregistry"
"localhost/client/go/outline/platerrors"
"localhost/client/go/outline/reporting"
"golang.getoutline.org/sdk/network"
"golang.getoutline.org/sdk/network/packetrelay"
"golang.getoutline.org/sdk/transport"
"github.com/goccy/go-yaml"
)
Expand All @@ -37,12 +37,12 @@ import (
// It's used by the connectivity test and the tun2socks handlers.
// TODO(fortuna):
// - Add connectivity test to StartSession()
// - Add NotifyNetworkChange() method. Needs to hold a network.PacketProxy instead of configregistry.PacketListener
// - Add NotifyNetworkChange() method. Needs to hold a packetrelay.PacketRelay instead of configregistry.PacketListener
// to handle that.
// - Refactor so that StartSession returns a Client
type Client struct {
sd *configregistry.Dialer[transport.StreamConn]
pp *configregistry.PacketProxy
pr *configregistry.PacketRelay
reporter reporting.Reporter
sessionCancel context.CancelFunc
}
Expand All @@ -52,14 +52,14 @@ func (c *Client) DialStream(ctx context.Context, address string) (transport.Stre
return c.sd.Dial(ctx, address)
}

// NewSession implements PacketProxy.NewSession.
func (c *Client) NewSession(resp network.PacketResponseReceiver) (network.PacketRequestSender, error) {
return c.pp.NewSession(resp)
// NewAssociation implements packetrelay.PacketRelay.NewAssociation.
func (c *Client) NewAssociation() (packetrelay.PacketSender, packetrelay.PacketReceiver, error) {
return c.pr.NewAssociation()
}

func (c *Client) NotifyNetworkChanged() {
if c.pp.NotifyNetworkChanged != nil {
c.pp.NotifyNetworkChanged()
if c.pr.NotifyNetworkChanged != nil {
c.pr.NotifyNetworkChanged()
}
}

Expand Down Expand Up @@ -155,7 +155,7 @@ func (c *ClientConfig) new(keyID string, providerClientConfigText string) (*Clie
}
}

client := &Client{sd: transportPair.StreamDialer, pp: transportPair.PacketProxy}
client := &Client{sd: transportPair.StreamDialer, pr: transportPair.PacketRelay}

// TODO: figure out a better way to handle parse calls.
if providerClientConfig.Reporter != nil {
Expand Down
26 changes: 13 additions & 13 deletions client/go/outline/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func Test_NewTransport_SS_URL(t *testing.T) {
result := (&ClientConfig{}).New("", config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, firstHop, result.Client.sd.FirstHop)
require.Equal(t, firstHop, result.Client.pp.FirstHop)
require.Equal(t, firstHop, result.Client.pr.FirstHop)
}

func Test_NewTransport_Legacy_JSON(t *testing.T) {
Expand All @@ -51,7 +51,7 @@ transport: {
result := (&ClientConfig{}).New("", config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, firstHop, result.Client.sd.FirstHop)
require.Equal(t, firstHop, result.Client.pp.FirstHop)
require.Equal(t, firstHop, result.Client.pr.FirstHop)
}

func Test_NewTransport_Flexible_JSON(t *testing.T) {
Expand All @@ -68,7 +68,7 @@ transport: {
result := (&ClientConfig{}).New("", config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, firstHop, result.Client.sd.FirstHop)
require.Equal(t, firstHop, result.Client.pp.FirstHop)
require.Equal(t, firstHop, result.Client.pr.FirstHop)
}

func Test_NewTransport_YAML(t *testing.T) {
Expand All @@ -84,7 +84,7 @@ transport:
result := (&ClientConfig{}).New("", config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, firstHop, result.Client.sd.FirstHop)
require.Equal(t, firstHop, result.Client.pp.FirstHop)
require.Equal(t, firstHop, result.Client.pr.FirstHop)
}

func Test_NewTransport_Explicit_endpoint(t *testing.T) {
Expand All @@ -100,7 +100,7 @@ transport:
result := (&ClientConfig{}).New("", config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, firstHop, result.Client.sd.FirstHop)
require.Equal(t, firstHop, result.Client.pp.FirstHop)
require.Equal(t, firstHop, result.Client.pr.FirstHop)
}

func Test_NewTransport_Multihop_URL(t *testing.T) {
Expand All @@ -117,7 +117,7 @@ transport:
result := (&ClientConfig{}).New("", config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, firstHop, result.Client.sd.FirstHop)
require.Equal(t, firstHop, result.Client.pp.FirstHop)
require.Equal(t, firstHop, result.Client.pr.FirstHop)
}

func Test_NewTransport_Multihop_Explicit(t *testing.T) {
Expand All @@ -138,7 +138,7 @@ transport:
result := (&ClientConfig{}).New("", config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, firstHop, result.Client.sd.FirstHop)
require.Equal(t, firstHop, result.Client.pp.FirstHop)
require.Equal(t, firstHop, result.Client.pr.FirstHop)
}

func Test_NewTransport_Explicit_TCPUDP(t *testing.T) {
Expand All @@ -160,7 +160,7 @@ transport:
result := (&ClientConfig{}).New("", config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, "example.com:80", result.Client.sd.FirstHop)
require.Equal(t, "example.com:53", result.Client.pp.FirstHop)
require.Equal(t, "example.com:53", result.Client.pr.FirstHop)
}

func Test_NewTransport_YAML_Reuse(t *testing.T) {
Expand All @@ -180,7 +180,7 @@ transport:
result := (&ClientConfig{}).New("", config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, firstHop, result.Client.sd.FirstHop)
require.Equal(t, firstHop, result.Client.pp.FirstHop)
require.Equal(t, firstHop, result.Client.pr.FirstHop)
}

func Test_NewTransport_YAML_Partial_Reuse(t *testing.T) {
Expand All @@ -202,7 +202,7 @@ transport:
result := (&ClientConfig{}).New("", config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, "example.com:80", result.Client.sd.FirstHop)
require.Equal(t, "example.com:53", result.Client.pp.FirstHop)
require.Equal(t, "example.com:53", result.Client.pr.FirstHop)
}

func Test_NewTransport_Unsupported(t *testing.T) {
Expand Down Expand Up @@ -233,7 +233,7 @@ transport:
result := (&ClientConfig{}).New("", config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, firstHop, result.Client.sd.FirstHop)
require.Equal(t, firstHop, result.Client.pp.FirstHop)
require.Equal(t, firstHop, result.Client.pr.FirstHop)
}

func Test_NewTransport_AllowProxyless(t *testing.T) {
Expand All @@ -246,7 +246,7 @@ transport:
require.Nil(t, result.Error, "Got %v", result.Error)
require.NotNil(t, result.Client)
require.Equal(t, configregistry.ConnTypeDirect, result.Client.sd.ConnType)
require.Equal(t, configregistry.ConnTypeDirect, result.Client.pp.ConnType)
require.Equal(t, configregistry.ConnTypeDirect, result.Client.pr.ConnType)
}

func Test_NewClientFromJSON_Errors(t *testing.T) {
Expand Down Expand Up @@ -334,7 +334,7 @@ reporter:
result := (&ClientConfig{}).New("", config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, "example.com:80", result.Client.sd.FirstHop)
require.Equal(t, "example.com:53", result.Client.pp.FirstHop)
require.Equal(t, "example.com:53", result.Client.pr.FirstHop)
require.NotNil(t, result.Client.reporter, "Reporter is nil")
request, err := result.Client.reporter.(*reporting.HTTPReporter).NewRequest()
require.NoError(t, err)
Expand Down
19 changes: 6 additions & 13 deletions client/go/outline/configregistry/config_proxyless.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ import (
"fmt"
"math/rand"

"localhost/client/go/configyaml"
"golang.getoutline.org/sdk/network"
"golang.getoutline.org/sdk/transport"
"golang.getoutline.org/sdk/transport/tlsfrag"
"localhost/client/go/configyaml"
)

const (
Expand Down Expand Up @@ -57,22 +56,16 @@ func parseProxylessTransportPair(ctx context.Context, configMap map[string]any,

splitLength := randomSplitLength()

sd, err := tlsfrag.NewFixedLenStreamDialer(&transport.TCPDialer{}, splitLength)
fragSD, err := tlsfrag.NewFixedLenStreamDialer(&transport.TCPDialer{}, splitLength)
if err != nil {
return nil, fmt.Errorf("failed to create StreamDialer: %w", err)
}

pl := &PacketListener{ConnectionProviderInfo{ConnTypeDirect, ""}, &transport.UDPListener{}}
pp, err := network.NewPacketProxyFromPacketListener(pl)
if err != nil {
return nil, fmt.Errorf("failed to create PacketProxy: %w", err)
sd := &Dialer[transport.StreamConn]{
ConnectionProviderInfo: ConnectionProviderInfo{ConnType: ConnTypeDirect},
Dial: fragSD.DialStream,
}

return &TransportPair{
StreamDialer: &Dialer[transport.StreamConn]{
ConnectionProviderInfo: ConnectionProviderInfo{ConnType: ConnTypeDirect},
Dial: sd.DialStream,
},
PacketProxy: &PacketProxy{ConnectionProviderInfo{ConnTypeDirect, ""}, pp, nil},
}, nil
return wrapTransportPairWithOutlineDNS(sd, pl)
}
6 changes: 3 additions & 3 deletions client/go/outline/configregistry/config_proxyless_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import (
"context"
"testing"

"localhost/client/go/configyaml"
"github.com/stretchr/testify/require"
"localhost/client/go/configyaml"
)

func TestParseProxyless(t *testing.T) {
Expand All @@ -32,7 +32,7 @@ func TestParseProxyless(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, transportPair)
require.NotNil(t, transportPair.StreamDialer)
require.NotNil(t, transportPair.PacketProxy)
require.NotNil(t, transportPair.PacketRelay)
require.Equal(t, ConnTypeDirect, transportPair.StreamDialer.ConnType)
require.Equal(t, ConnTypeDirect, transportPair.PacketProxy.ConnType)
require.Equal(t, ConnTypeDirect, transportPair.PacketRelay.ConnType)
}
51 changes: 30 additions & 21 deletions client/go/outline/configregistry/outline_dns_intercept.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,18 @@
package configregistry

import (
"context"
"fmt"
"log/slog"
"math/rand/v2"
"net/netip"
"time"

"localhost/client/go/outline/connectivity"
"localhost/client/go/outline/dnsintercept"

"golang.getoutline.org/sdk/network"
"golang.getoutline.org/sdk/network/dnsintercept"
"golang.getoutline.org/sdk/network/dnstruncate"
"golang.getoutline.org/sdk/network/packetrelay"
"golang.getoutline.org/sdk/transport"
)

Expand All @@ -50,46 +53,52 @@ func wrapTransportPairWithOutlineDNS(sd *Dialer[transport.StreamConn], pl *Packe
// Randomly selects a DNS resolver for the VPN session
remoteDNS := outlineDNSResolvers[rand.IntN(len(outlineDNSResolvers))]

// Intercept DNS for StreamDialer
sdForward, err := dnsintercept.NewDNSRedirectStreamDialer(transport.FuncStreamDialer(sd.Dial), linkLocalDNS, remoteDNS)
if err != nil {
return nil, fmt.Errorf("failed to create DNS redirect StreamDialer: %w", err)
// Intercept DNS for StreamDialer: remap TCP connections to linkLocalDNS → remoteDNS.
sdForward := func(ctx context.Context, addr string) (transport.StreamConn, error) {
if dst, err := netip.ParseAddrPort(addr); err == nil && dst.Addr().Unmap() == linkLocalDNS.Addr() && dst.Port() == linkLocalDNS.Port() {
addr = remoteDNS.String()
}
return sd.Dial(ctx, addr)
}

// Intercept DNS for PacketProxy
ppBase, err := network.NewPacketProxyFromPacketListener(pl)
baseListener, err := packetrelay.NewPacketRelayFromPacketListener(pl.PacketListener, 30*time.Second)
if err != nil {
return nil, fmt.Errorf("failed to create PacketProxy: %w", err)
return nil, fmt.Errorf("failed to create base PacketRelay: %w", err)
}
// Forwards everything including DNS. For DNS it translates between the link-local and remote addresses for the DNS resolver.
ppForward, err := dnsintercept.NewDNSRedirectPacketProxy(ppBase, linkLocalDNS, remoteDNS)
// Forward relay: intercept DNS at link-local address, forward to remote resolver.
// DNS gets a shorter 5s timeout on its own independent listener.
dnsListener, err := packetrelay.NewPacketRelayFromPacketListener(pl.PacketListener, 5*time.Second)
if err != nil {
return nil, fmt.Errorf("failed to create DNS redirect PacketProxy: %w", err)
return nil, fmt.Errorf("failed to create DNS PacketRelay: %w", err)
}
// Forwards everything except DNS. For DNS it returns a truncated response.
ppTrunc, err := dnsintercept.NewDNSTruncatePacketProxy(ppBase, linkLocalDNS)
relayForward := dnsintercept.NewInterceptDNSPacketRelay(dnsListener, baseListener, linkLocalDNS, remoteDNS)
// Truncate relay: intercept DNS at link-local address, return truncated response (forces TCP retry).
// Non-DNS traffic passes through to baseListener.
dnsTruncRelay, err := dnstruncate.NewPacketRelay()
if err != nil {
return nil, fmt.Errorf("failed to create always-truncate DNS PacketProxy: %w", err)
return nil, fmt.Errorf("failed to create DNS truncate relay: %w", err)
}
ppMain, err := network.NewDelegatePacketProxy(ppTrunc)
relayTrunc := dnsintercept.NewInterceptDNSPacketRelay(dnsTruncRelay, baseListener, linkLocalDNS, remoteDNS)
// Delegate relay starts with truncate (UDP unverified), switches to forward when UDP is healthy.
relayMain, err := packetrelay.NewDelegatePacketRelay(relayTrunc)
if err != nil {
return nil, fmt.Errorf("failed to create indirect PacketProxy: %w", err)
return nil, fmt.Errorf("failed to create delegate PacketRelay: %w", err)
}

onNetworkChanged := func() {
go func() {
if err := connectivity.CheckUDPConnectivity(pl); err == nil {
slog.Info("remote device UDP is healthy")
ppMain.SetProxy(ppForward)
relayMain.SetRelay(relayForward)
} else {
slog.Warn("remote device UDP is not healthy", "err", err)
ppMain.SetProxy(ppTrunc)
relayMain.SetRelay(relayTrunc)
}
Comment thread
fortuna marked this conversation as resolved.
}()
}

return &TransportPair{
&Dialer[transport.StreamConn]{sd.ConnectionProviderInfo, sdForward.DialStream},
&PacketProxy{pl.ConnectionProviderInfo, ppMain, onNetworkChanged},
&Dialer[transport.StreamConn]{sd.ConnectionProviderInfo, sdForward},
&PacketRelay{pl.ConnectionProviderInfo, relayMain, onNetworkChanged},
}, nil
}
18 changes: 9 additions & 9 deletions client/go/outline/configregistry/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ udp: *shared`)
require.NoError(t, err)

require.NotNil(t, d.StreamDialer)
require.NotNil(t, d.PacketProxy)
require.NotNil(t, d.PacketRelay)
require.Equal(t, "example.com:1234", d.StreamDialer.FirstHop)
require.Equal(t, ConnTypeTunneled, d.StreamDialer.ConnType)
require.Equal(t, "example.com:1234", d.PacketProxy.FirstHop)
require.Equal(t, ConnTypeTunneled, d.PacketProxy.ConnType)
require.Equal(t, "example.com:1234", d.PacketRelay.FirstHop)
require.Equal(t, ConnTypeTunneled, d.PacketRelay.ConnType)
}

func TestRegisterParseURL(t *testing.T) {
Expand All @@ -65,11 +65,11 @@ func TestRegisterParseURL(t *testing.T) {
require.NoError(t, err)

require.NotNil(t, d.StreamDialer)
require.NotNil(t, d.PacketProxy)
require.NotNil(t, d.PacketRelay)
require.Equal(t, "example.com:4321", d.StreamDialer.FirstHop)
require.Equal(t, ConnTypeTunneled, d.StreamDialer.ConnType)
require.Equal(t, "example.com:4321", d.PacketProxy.FirstHop)
require.Equal(t, ConnTypeTunneled, d.PacketProxy.ConnType)
require.Equal(t, "example.com:4321", d.PacketRelay.FirstHop)
require.Equal(t, ConnTypeTunneled, d.PacketRelay.ConnType)
}

func TestRegisterParseURLInQuotes(t *testing.T) {
Expand All @@ -82,11 +82,11 @@ func TestRegisterParseURLInQuotes(t *testing.T) {
require.NoError(t, err)

require.NotNil(t, d.StreamDialer)
require.NotNil(t, d.PacketProxy)
require.NotNil(t, d.PacketRelay)
require.Equal(t, "example.com:4321", d.StreamDialer.FirstHop)
require.Equal(t, ConnTypeTunneled, d.StreamDialer.ConnType)
require.Equal(t, "example.com:4321", d.PacketProxy.FirstHop)
require.Equal(t, ConnTypeTunneled, d.PacketProxy.ConnType)
require.Equal(t, "example.com:4321", d.PacketRelay.FirstHop)
require.Equal(t, ConnTypeTunneled, d.PacketRelay.ConnType)
}

type errorStreamDialer struct {
Expand Down
10 changes: 5 additions & 5 deletions client/go/outline/configregistry/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
"context"
"encoding/json"

"golang.getoutline.org/sdk/network"
"golang.getoutline.org/sdk/network/packetrelay"
"golang.getoutline.org/sdk/transport"
)

Expand Down Expand Up @@ -72,10 +72,10 @@ type PacketListener struct {
transport.PacketListener
}

// PacketProxy is a [network.PacketProxy] with embedded ConnectionProviderInfo.
type PacketProxy struct {
// PacketRelay is a [packetrelay.PacketRelay] with embedded ConnectionProviderInfo.
type PacketRelay struct {
ConnectionProviderInfo
network.PacketProxy
packetrelay.PacketRelay
NotifyNetworkChanged func()
}

Expand All @@ -102,7 +102,7 @@ type Endpoint[ConnType any] struct {
// TransportPair provides a StreamDialer and PacketListener, to use as the transport in a Tun2Socks VPN.
type TransportPair struct {
StreamDialer *Dialer[transport.StreamConn]
PacketProxy *PacketProxy
PacketRelay *PacketRelay
}

var _ transport.StreamDialer = (*TransportPair)(nil)
Expand Down
Loading
Loading