Skip to content

Commit 24de087

Browse files
myleshortonclaude
andcommitted
Add UDP-over-TCP (UoT) support to samizdat protocol
Tunnel UDP traffic over samizdat's TCP connections using the standard sing UoT protocol, following the AnyTLS pattern. Inbound wraps the router with uot.NewRouter to intercept the UoT magic address; outbound adds a uot.Client backed by a samizdatDialer adapter so sing-box can route both TCP and UDP through samizdat. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3afa00a commit 24de087

File tree

3 files changed

+53
-15
lines changed

3 files changed

+53
-15
lines changed

protocol/samizdat/inbound.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/sagernet/sing-box/adapter"
1313
"github.com/sagernet/sing-box/adapter/inbound"
14+
"github.com/sagernet/sing-box/common/uot"
1415
"github.com/sagernet/sing-box/log"
1516
M "github.com/sagernet/sing/common/metadata"
1617

@@ -30,7 +31,7 @@ type Inbound struct {
3031
inbound.Adapter
3132
ctx context.Context
3233
logger log.ContextLogger
33-
router adapter.Router
34+
router adapter.ConnectionRouterEx
3435
server *samizdat.Server
3536
}
3637

@@ -115,7 +116,7 @@ func NewInbound(
115116
Adapter: inbound.NewAdapter(constant.TypeSamizdat, tag),
116117
ctx: ctx,
117118
logger: logger,
118-
router: router,
119+
router: uot.NewRouter(router, logger),
119120
}
120121

121122
serverConfig := samizdat.ServerConfig{

protocol/samizdat/outbound.go

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/hex"
66
"fmt"
77
"net"
8+
"os"
89
"time"
910

1011
"github.com/sagernet/sing-box/adapter"
@@ -14,6 +15,7 @@ import (
1415
"github.com/sagernet/sing/common/logger"
1516
M "github.com/sagernet/sing/common/metadata"
1617
N "github.com/sagernet/sing/common/network"
18+
"github.com/sagernet/sing/common/uot"
1719

1820
"github.com/getlantern/lantern-box/constant"
1921
"github.com/getlantern/lantern-box/option"
@@ -29,10 +31,24 @@ func RegisterOutbound(registry *outbound.Registry) {
2931
// Outbound represents a Samizdat outbound adapter.
3032
type Outbound struct {
3133
outbound.Adapter
32-
logger logger.ContextLogger
34+
logger logger.ContextLogger
35+
client *samizdat.Client
36+
uotClient *uot.Client
37+
}
38+
39+
// samizdatDialer adapts a samizdat.Client to the N.Dialer interface for uot.Client.
40+
type samizdatDialer struct {
3341
client *samizdat.Client
3442
}
3543

44+
func (d *samizdatDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
45+
return d.client.DialContext(ctx, network, destination.String())
46+
}
47+
48+
func (d *samizdatDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
49+
return nil, os.ErrInvalid
50+
}
51+
3652
// NewOutbound creates a new Samizdat outbound adapter.
3753
func NewOutbound(
3854
ctx context.Context,
@@ -107,16 +123,23 @@ func NewOutbound(
107123
return nil, fmt.Errorf("creating samizdat client: %w", err)
108124
}
109125

110-
return &Outbound{
126+
o := &Outbound{
111127
Adapter: outbound.NewAdapterWithDialerOptions(
112128
constant.TypeSamizdat,
113129
tag,
114-
[]string{N.NetworkTCP},
130+
[]string{N.NetworkTCP, N.NetworkUDP},
115131
options.DialerOptions,
116132
),
117133
logger: logger,
118134
client: client,
119-
}, nil
135+
}
136+
137+
o.uotClient = &uot.Client{
138+
Dialer: &samizdatDialer{client: client},
139+
Version: uot.Version,
140+
}
141+
142+
return o, nil
120143
}
121144

122145
// DialContext dials a connection to the destination through the Samizdat proxy.
@@ -125,23 +148,29 @@ func (o *Outbound) DialContext(ctx context.Context, network string, destination
125148
metadata.Outbound = o.Tag()
126149
metadata.Destination = destination
127150

128-
o.logger.InfoContext(ctx, "connecting to ", destination)
129-
conn, err := o.client.DialContext(ctx, network, destination.String())
130-
if err != nil {
131-
return nil, fmt.Errorf("samizdat dial to %s: %w", destination, err)
151+
switch N.NetworkName(network) {
152+
case N.NetworkTCP:
153+
o.logger.InfoContext(ctx, "outbound connection to ", destination)
154+
return o.client.DialContext(ctx, network, destination.String())
155+
case N.NetworkUDP:
156+
o.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
157+
return o.uotClient.DialContext(ctx, network, destination)
132158
}
133-
134-
return conn, nil
159+
return nil, fmt.Errorf("unsupported network: %s", network)
135160
}
136161

137-
// ListenPacket is not supported by Samizdat (TCP-only protocol).
162+
// ListenPacket creates a UoT packet connection through the Samizdat proxy.
138163
func (o *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
139-
return nil, fmt.Errorf("samizdat does not support UDP")
164+
ctx, metadata := adapter.ExtendContext(ctx)
165+
metadata.Outbound = o.Tag()
166+
metadata.Destination = destination
167+
o.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
168+
return o.uotClient.ListenPacket(ctx, destination)
140169
}
141170

142171
// Network returns the supported network types.
143172
func (o *Outbound) Network() []string {
144-
return []string{N.NetworkTCP}
173+
return []string{N.NetworkTCP, N.NetworkUDP}
145174
}
146175

147176
// Close shuts down the Samizdat client.

protocol/samizdat/samizdat_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,14 @@ func TestNewOutbound_InvalidPublicKey(t *testing.T) {
296296
}
297297
}
298298

299+
func TestOutbound_NetworkIncludesUDP(t *testing.T) {
300+
o := &Outbound{}
301+
networks := o.Network()
302+
assert.Contains(t, networks, "tcp", "Network() should include TCP")
303+
assert.Contains(t, networks, "udp", "Network() should include UDP")
304+
assert.Len(t, networks, 2, "Network() should return exactly 2 networks")
305+
}
306+
299307
func TestNewOutbound_InvalidShortID(t *testing.T) {
300308
pubKey := testPubKeyHex(t)
301309

0 commit comments

Comments
 (0)