Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: move IP validation to the PacketListener #229

Open
wants to merge 84 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
b2aa859
refactor: split UDP serving from handling of packets
sbruens Nov 18, 2024
c0c0e9f
Use a read channel.
sbruens Nov 20, 2024
8d95309
Rename `wrappedPacketConn` to just `packetConn`.
sbruens Nov 20, 2024
51d5c88
Update `shadowsocks_handler`.
sbruens Nov 21, 2024
422542b
Change the `HandlePacket` API to not require the `pkt`.
sbruens Nov 22, 2024
c10176f
Remove the NAT map from the `Handle()` function.
sbruens Nov 25, 2024
004c6c3
Rework the metrics now that the NAT is no longer done by the handler.
sbruens Nov 26, 2024
a91d55b
Move the metrics `AddClosed()` call to after the `timedCopy` returns.
sbruens Nov 26, 2024
1d7200b
Remove the explicit `targetConn.Close()` and let it expire on its own.
sbruens Nov 26, 2024
4b8eeae
Add the NAT metrics back in at the server level.
sbruens Nov 27, 2024
1e89e85
Use buffer pool on `packetHandler` instead of global.
sbruens Nov 27, 2024
b4e7dbb
Revert wrapping the `clientConn` with shadowsocks encryption/decryption.
sbruens Nov 27, 2024
63c2cff
Rename `packet` to `association`.
sbruens Nov 27, 2024
1eeb4d6
Fix tests.
sbruens Nov 28, 2024
08e8bba
Update the docstring for `PacketServe`.
sbruens Nov 28, 2024
34a01d7
Fix comment to refer to "associations".
sbruens Nov 28, 2024
033ba9d
Fix metrics test.
sbruens Dec 3, 2024
87a9d23
Use `slicepool`.
sbruens Dec 3, 2024
50feaa2
Let the assocation handler provide the buffer.
sbruens Dec 4, 2024
b7a4a3c
Remove the `packetConnWrapper` and move the logic into `natconn` inst…
sbruens Dec 10, 2024
f80294c
Fix close while reading of `natconn`.
sbruens Dec 10, 2024
36d4b27
Simplify the natmap a little.
sbruens Dec 10, 2024
f5d9ac3
Refactor `PacketServe` to use events (close and read).
sbruens Dec 13, 2024
e0547f2
Use correct logger.
sbruens Dec 13, 2024
3dc12d1
Keep `readCh` and `closeCh` unbuffered.
sbruens Dec 16, 2024
afb9cd1
Catch panics in the `ReadFrom` go routine.
sbruens Dec 16, 2024
5d2acc6
Close the `readCh` instead of sending the error on the `readCh`.
sbruens Dec 16, 2024
7cd0f1f
Wrap a logger with the association's client address so we can simplif…
sbruens Dec 16, 2024
078032c
Reference GitHub issue for supporting multiple IPs.
sbruens Dec 16, 2024
2b3f746
Simplify packet handling with a new `association` struct.
sbruens Dec 18, 2024
2f2268b
Add some comments to the `Association` interface.
sbruens Dec 18, 2024
bfe4b35
Consolidate debug logging.
sbruens Dec 18, 2024
f37f23a
Rename some vars.
sbruens Dec 18, 2024
08de4c3
Update doc.
sbruens Dec 18, 2024
418c0e4
Format.
sbruens Dec 19, 2024
98988b0
Do not unpack first packets twice.
sbruens Dec 19, 2024
b9c0286
Move handling into the association.
sbruens Dec 20, 2024
d14ea20
Merge branch 'master' into sbruens/udp-split-serving
sbruens Dec 20, 2024
eef630a
Add some comments to the timeout value.
sbruens Dec 20, 2024
1c462b1
Don't set the stream dialer in the old config flow.
sbruens Dec 20, 2024
0918050
Rename `AddAuthentication` and `AddClose`.
sbruens Jan 6, 2025
78af4be
Update comment to reflect it handles packets from both directions.
sbruens Jan 6, 2025
2cf398c
Separate the interfaces for `Handle()` and `HandlePacket()`.
sbruens Jan 6, 2025
1055cb1
Fix typo in `UDPAssocationMetrics`.
sbruens Jan 6, 2025
f8ab81a
Don't pass `conn` to `Handle()`.
sbruens Jan 6, 2025
e6ef2f3
Update comment.
sbruens Jan 6, 2025
2939f8a
Add comments.
sbruens Jan 6, 2025
23a03e9
Remove ConnAssociation in favor of a `HandleAssociation(Conn, PacketA…
sbruens Jan 8, 2025
aa130f0
Split `Service` interface into outline-ss-server and Caddy interfaces.
sbruens Jan 8, 2025
19fb5f7
Remove unused const.
sbruens Jan 8, 2025
c656b36
Don't require `conn` in `HandleAssociation()`.
sbruens Jan 8, 2025
31cec7c
Exit the loop if the connection is closed.
sbruens Jan 9, 2025
0642698
Re-use global buffer pool.
sbruens Jan 10, 2025
a16c1b3
Remove app-specific interfaces.
sbruens Jan 10, 2025
77983fc
Decouple the association and the packet handling.
sbruens Jan 11, 2025
f3d63ae
Move timedCopy handling to the packet handler.
sbruens Jan 11, 2025
4c4072c
Remove unused property.
sbruens Jan 13, 2025
413a1aa
Decouple shadowsocks from association.
sbruens Jan 13, 2025
db3d0a3
Move authentication into its own function.
sbruens Jan 13, 2025
5f685bb
Remove the `packetMetrics` struct.
sbruens Jan 13, 2025
1dbfa99
Update tests.
sbruens Jan 13, 2025
23050ef
Remove the `Metrics()` method.
sbruens Jan 13, 2025
47222f6
Move variables into the anonymous functions.
sbruens Jan 16, 2025
8e9af05
Rename stream and packet handlers `Handle()` methods.
sbruens Jan 17, 2025
5704718
More `handle` naming clarification.
sbruens Jan 17, 2025
0cbcd61
Refactor to make packet handler an association handler.
sbruens Jan 21, 2025
bcac22b
Only handle the association if it was new.
sbruens Jan 21, 2025
220d1d7
Fix the metric race condition in tests.
sbruens Jan 21, 2025
d81f128
Move `AddNATEntry()` call to new entry only.
sbruens Jan 21, 2025
96de2a6
Format.
sbruens Jan 22, 2025
09e471f
Address review comments.
sbruens Jan 22, 2025
64c48ce
Make `clientConn` an `io.Writer`.
sbruens Jan 22, 2025
9f2cdb1
refactor: move IP validation to the PacketListener
sbruens Jan 22, 2025
76789e7
Rename `validatePacket()` to reflect what it currently does.
sbruens Jan 22, 2025
de27a32
Reorder for consistency.
sbruens Jan 22, 2025
a62e0b6
Handle the `EOF` case and stop reading from the connection.
sbruens Jan 23, 2025
dd8a510
Merge branch 'sbruens/udp-split-serving' into sbruens/udp-ip-validator
sbruens Jan 23, 2025
f67f217
Pass through the connection errors from the IP validator.
sbruens Jan 23, 2025
2427670
Address review comments.
sbruens Jan 24, 2025
b2482c2
Merge branch 'sbruens/udp-split-serving' into sbruens/udp-ip-validator
sbruens Jan 24, 2025
07c1c41
Rename `timeout` to `natTimeout` in test.
sbruens Jan 24, 2025
b077e55
Only resolve the `net.Addr` if it's not already a `UDPAddr`.
sbruens Jan 24, 2025
b866030
Use `MakeNetAddr()` instead of resolving when extracting the addr.
sbruens Jan 24, 2025
f1d7bef
Merge branch 'master' into sbruens/udp-ip-validator
sbruens Jan 24, 2025
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
4 changes: 2 additions & 2 deletions cmd/outline-ss-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) {
service.WithCiphers(ciphers),
service.WithMetrics(s.serviceMetrics),
service.WithReplayCache(&s.replayCache),
service.WithPacketListener(service.MakeTargetUDPListener(s.natTimeout, 0)),
service.WithPacketListener(service.MakeTargetUDPListener(onet.RequirePublicIP, s.natTimeout, 0)),
service.WithLogger(slog.Default()),
)
ln, err := lnSet.ListenStream(addr)
Expand Down Expand Up @@ -261,7 +261,7 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) {
service.WithMetrics(s.serviceMetrics),
service.WithReplayCache(&s.replayCache),
service.WithStreamDialer(service.MakeValidatingTCPStreamDialer(onet.RequirePublicIP, serviceConfig.Dialer.Fwmark)),
service.WithPacketListener(service.MakeTargetUDPListener(s.natTimeout, serviceConfig.Dialer.Fwmark)),
service.WithPacketListener(service.MakeTargetUDPListener(onet.RequirePublicIP, s.natTimeout, serviceConfig.Dialer.Fwmark)),
service.WithLogger(slog.Default()),
)
if err != nil {
Expand Down
11 changes: 7 additions & 4 deletions internal/integration_test/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ import (
"github.com/stretchr/testify/require"
)

const maxUDPPacketSize = 64 * 1024
const (
maxUDPPacketSize = 64 * 1024
natTimeout = 5 * time.Minute
)

func init() {
logging.SetLevel(logging.INFO, "")
Expand Down Expand Up @@ -321,7 +324,7 @@ func TestUDPEcho(t *testing.T) {
}
proxy := service.NewAssociationHandler(cipherList, &fakeShadowsocksMetrics{})

proxy.SetTargetIPValidator(allowAll)
proxy.SetTargetPacketListener(service.MakeTargetUDPListener(allowAll, natTimeout, 0))
natMetrics := &natTestMetrics{}
associationMetrics := &fakeUDPAssociationMetrics{}
go service.PacketServe(proxyConn, func(ctx context.Context, conn net.Conn) {
Expand Down Expand Up @@ -548,7 +551,7 @@ func BenchmarkUDPEcho(b *testing.B) {
b.Fatal(err)
}
proxy := service.NewAssociationHandler(cipherList, &fakeShadowsocksMetrics{})
proxy.SetTargetIPValidator(allowAll)
proxy.SetTargetPacketListener(service.MakeTargetUDPListener(allowAll, natTimeout, 0))
done := make(chan struct{})
go func() {
service.PacketServe(server, func(ctx context.Context, conn net.Conn) {
Expand Down Expand Up @@ -594,7 +597,7 @@ func BenchmarkUDPManyKeys(b *testing.B) {
b.Fatal(err)
}
proxy := service.NewAssociationHandler(cipherList, &fakeShadowsocksMetrics{})
proxy.SetTargetIPValidator(allowAll)
proxy.SetTargetPacketListener(service.MakeTargetUDPListener(allowAll, natTimeout, 0))
done := make(chan struct{})
go func() {
service.PacketServe(proxyConn, func(ctx context.Context, conn net.Conn) {
Expand Down
49 changes: 28 additions & 21 deletions service/udp.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func NewAssociationHandler(cipherList CipherList, ssMetrics ShadowsocksConnMetri
ciphers: cipherList,
ssm: ssMetrics,
targetIPValidator: onet.RequirePublicIP,
targetListener: MakeTargetUDPListener(defaultNatTimeout, 0),
targetListener: MakeTargetUDPListener(onet.RequirePublicIP, defaultNatTimeout, 0),
}
}

Expand All @@ -118,8 +118,6 @@ type AssociationHandler interface {
HandleAssociation(ctx context.Context, conn net.Conn, assocMetrics UDPAssociationMetrics)
// SetLogger sets the logger used to log messages. Uses a no-op logger if nil.
SetLogger(l *slog.Logger)
// SetTargetIPValidator sets the function to be used to validate the target IP addresses.
SetTargetIPValidator(targetIPValidator onet.TargetIPValidator)
// SetTargetPacketListener sets the packet listener to use for target connections.
SetTargetPacketListener(targetListener transport.PacketListener)
}
Expand All @@ -131,10 +129,6 @@ func (h *associationHandler) SetLogger(l *slog.Logger) {
h.logger = l
}

func (h *associationHandler) SetTargetIPValidator(targetIPValidator onet.TargetIPValidator) {
h.targetIPValidator = targetIPValidator
}

func (h *associationHandler) SetTargetPacketListener(targetListener transport.PacketListener) {
h.targetListener = targetListener
}
Expand Down Expand Up @@ -171,7 +165,7 @@ func (h *associationHandler) HandleAssociation(ctx context.Context, clientConn n

connError := func() *onet.ConnectionError {
var payload []byte
var tgtUDPAddr *net.UDPAddr
var tgtAddr net.Addr
if targetConn == nil {
ip := clientConn.RemoteAddr().(*net.UDPAddr).AddrPort().Addr()
var textData []byte
Expand All @@ -189,7 +183,7 @@ func (h *associationHandler) HandleAssociation(ctx context.Context, clientConn n
assocMetrics.AddAuthentication(keyID)

var onetErr *onet.ConnectionError
if payload, tgtUDPAddr, onetErr = h.validatePacket(textData); onetErr != nil {
if payload, tgtAddr, onetErr = h.extractPayloadAndDestination(textData); onetErr != nil {
return onetErr
}

Expand All @@ -211,15 +205,15 @@ func (h *associationHandler) HandleAssociation(ctx context.Context, clientConn n
}

var onetErr *onet.ConnectionError
if payload, tgtUDPAddr, onetErr = h.validatePacket(textData); onetErr != nil {
if payload, tgtAddr, onetErr = h.extractPayloadAndDestination(textData); onetErr != nil {
return onetErr
}
}

debugUDP(l, "Proxy exit.")
proxyTargetBytes, err = targetConn.WriteTo(payload, tgtUDPAddr) // accept only UDPAddr despite the signature
sbruens marked this conversation as resolved.
Show resolved Hide resolved
proxyTargetBytes, err = targetConn.WriteTo(payload, tgtAddr)
if err != nil {
return onet.NewConnectionError("ERR_WRITE", "Failed to write to target", err)
return ensureConnectionError(err, "ERR_WRITE", "Failed to write to target")
}
return nil
}()
Expand All @@ -233,21 +227,17 @@ func (h *associationHandler) HandleAssociation(ctx context.Context, clientConn n
}
}

// Given the decrypted contents of a UDP packet, return
// the payload and the destination address, or an error if
// this packet cannot or should not be forwarded.
func (h *associationHandler) validatePacket(textData []byte) ([]byte, *net.UDPAddr, *onet.ConnectionError) {
// extractPayloadAndDestination processes a decrypted Shadowsocks UDP packet and
// extracts the payload data and destination address.
func (h *associationHandler) extractPayloadAndDestination(textData []byte) ([]byte, net.Addr, *onet.ConnectionError) {
tgtAddr := socks.SplitAddr(textData)
if tgtAddr == nil {
return nil, nil, onet.NewConnectionError("ERR_READ_ADDRESS", "Failed to get target address", nil)
}

tgtUDPAddr, err := net.ResolveUDPAddr("udp", tgtAddr.String())
tgtUDPAddr, err := transport.MakeNetAddr("udp", tgtAddr.String())
if err != nil {
sbruens marked this conversation as resolved.
Show resolved Hide resolved
return nil, nil, onet.NewConnectionError("ERR_RESOLVE_ADDRESS", fmt.Sprintf("Failed to resolve target address %v", tgtAddr), err)
}
if err := h.targetIPValidator(tgtUDPAddr.IP); err != nil {
return nil, nil, ensureConnectionError(err, "ERR_ADDRESS_INVALID", "invalid address")
return nil, nil, onet.NewConnectionError("ERR_CONVERT_ADDRESS", fmt.Sprintf("Failed to convert target address %v", tgtAddr), err)
}

payload := textData[len(tgtAddr):]
Expand Down Expand Up @@ -395,6 +385,23 @@ func isDNS(addr net.Addr) bool {
return port == "53"
}

type validatingPacketConn struct {
net.PacketConn
targetIPValidator onet.TargetIPValidator
}

func (vpc *validatingPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) {
udpAddr, err := net.ResolveUDPAddr("udp", addr.String())
if err != nil {
return 0, onet.NewConnectionError("ERR_RESOLVE_ADDRESS", fmt.Sprintf("Failed to resolve target address %v", udpAddr), err)
}
if err := vpc.targetIPValidator(udpAddr.IP); err != nil {
return 0, ensureConnectionError(err, "ERR_ADDRESS_INVALID", "invalid address")
}

return vpc.PacketConn.WriteTo(p, udpAddr) // accept only `net.UDPAddr` despite the signature
}

type timedPacketConn struct {
net.PacketConn
// Connection timeout to apply for non-DNS packets.
Expand Down
19 changes: 14 additions & 5 deletions service/udp_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,18 @@ import (
"time"

"github.com/Jigsaw-Code/outline-sdk/transport"

onet "github.com/Jigsaw-Code/outline-ss-server/net"
)

type udpListener struct {
// The validator to be used to validate target IP addresses.
targetIPValidator onet.TargetIPValidator

// NAT mapping timeout is the default time a mapping will stay active
// without packets traversing the NAT, applied to non-DNS packets.
timeout time.Duration

// fwmark can be used in conjunction with other Linux networking features like cgroups, network
// namespaces, and TC (Traffic Control) for sophisticated network management.
// Value of 0 disables fwmark (SO_MARK) (Linux only)
Expand All @@ -37,14 +43,14 @@ type udpListener struct {

// NewPacketListener creates a new PacketListener that listens on UDP
// and optionally sets a firewall mark on the socket (Linux only).
func MakeTargetUDPListener(timeout time.Duration, fwmark uint) transport.PacketListener {
return &udpListener{timeout: timeout, fwmark: fwmark}
func MakeTargetUDPListener(targetIPValidator onet.TargetIPValidator, timeout time.Duration, fwmark uint) transport.PacketListener {
return &udpListener{timeout: timeout, targetIPValidator: targetIPValidator, fwmark: fwmark}
}

func (ln *udpListener) ListenPacket(ctx context.Context) (net.PacketConn, error) {
conn, err := net.ListenUDP("udp", nil)
if err != nil {
return nil, fmt.Errorf("Failed to create UDP socket: %w", err)
return nil, fmt.Errorf("failed to create UDP socket: %w", err)
}

if ln.fwmark > 0 {
Expand All @@ -57,9 +63,12 @@ func (ln *udpListener) ListenPacket(ctx context.Context) (net.PacketConn, error)
err = SetFwmark(rawConn, ln.fwmark)
if err != nil {
conn.Close()
return nil, fmt.Errorf("Failed to set `fwmark`: %w", err)
return nil, fmt.Errorf("failed to set `fwmark`: %w", err)

}
}
return &timedPacketConn{PacketConn: conn, defaultTimeout: ln.timeout}, nil
return &validatingPacketConn{
PacketConn: &timedPacketConn{PacketConn: conn, defaultTimeout: ln.timeout},
targetIPValidator: ln.targetIPValidator,
}, nil
}
18 changes: 15 additions & 3 deletions service/udp_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,41 @@ import (
"time"

"github.com/Jigsaw-Code/outline-sdk/transport"

onet "github.com/Jigsaw-Code/outline-ss-server/net"
)

type udpListener struct {
*transport.UDPListener

// The validator to be used to validate target IP addresses.
targetIPValidator onet.TargetIPValidator

// NAT mapping timeout is the default time a mapping will stay active
// without packets traversing the NAT, applied to non-DNS packets.
timeout time.Duration
}

// fwmark can be used in conjunction with other Linux networking features like cgroups, network namespaces, and TC (Traffic Control) for sophisticated network management.
// Value of 0 disables fwmark (SO_MARK)
func MakeTargetUDPListener(timeout time.Duration, fwmark uint) transport.PacketListener {
func MakeTargetUDPListener(targetIPValidator onet.TargetIPValidator, timeout time.Duration, fwmark uint) transport.PacketListener {
if fwmark != 0 {
panic("fwmark is linux-specific feature and should be 0")
}
return &udpListener{UDPListener: &transport.UDPListener{Address: ""}}
return &udpListener{
targetIPValidator: targetIPValidator,
timeout: timeout,
UDPListener: &transport.UDPListener{Address: ""},
}
}

func (ln *udpListener) ListenPacket(ctx context.Context) (net.PacketConn, error) {
conn, err := ln.UDPListener.ListenPacket(ctx)
if err != nil {
return nil, err
}
return &timedPacketConn{PacketConn: conn, defaultTimeout: ln.timeout}, nil
return &validatingPacketConn{
PacketConn: &timedPacketConn{PacketConn: conn, defaultTimeout: ln.timeout},
targetIPValidator: ln.targetIPValidator,
}, nil
}
14 changes: 12 additions & 2 deletions service/udp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"testing"
"time"

"github.com/Jigsaw-Code/outline-sdk/transport"
"github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks"
logging "github.com/op/go-logging"
"github.com/shadowsocks/go-shadowsocks2/socks"
Expand Down Expand Up @@ -62,6 +63,15 @@ func (ln *packetListener) ListenPacket(ctx context.Context) (net.PacketConn, err
return ln.conn, nil
}

func WrapWithValidatingPacketListener(conn net.PacketConn, targetIPValidator onet.TargetIPValidator) transport.PacketListener {
return &packetListener{
&validatingPacketConn{
PacketConn: conn,
targetIPValidator: targetIPValidator,
},
}
}

type fakePacketConn struct {
net.PacketConn
send chan fakePacket
Expand Down Expand Up @@ -225,7 +235,7 @@ func TestAssociationCloseWhileReading(t *testing.T) {
func TestAssociationHandler_Handle_IPFilter(t *testing.T) {
t.Run("RequirePublicIP blocks localhost", func(t *testing.T) {
handler, sendPayload, targetConn := startTestHandler()
handler.SetTargetIPValidator(onet.RequirePublicIP)
handler.SetTargetPacketListener(WrapWithValidatingPacketListener(targetConn, onet.RequirePublicIP))

sendPayload(&localAddr, []byte{1, 2, 3})

Expand All @@ -239,7 +249,7 @@ func TestAssociationHandler_Handle_IPFilter(t *testing.T) {

t.Run("allowAll allows localhost", func(t *testing.T) {
handler, sendPayload, targetConn := startTestHandler()
handler.SetTargetIPValidator(allowAll)
handler.SetTargetPacketListener(WrapWithValidatingPacketListener(targetConn, allowAll))

sendPayload(&localAddr, []byte{1, 2, 3})

Expand Down
Loading