Skip to content

Commit

Permalink
Merge branch 'master' into sbruens/udp-split-serving
Browse files Browse the repository at this point in the history
  • Loading branch information
sbruens committed Dec 20, 2024
2 parents b9c0286 + 98db5b4 commit d14ea20
Show file tree
Hide file tree
Showing 14 changed files with 371 additions and 131 deletions.
6 changes: 6 additions & 0 deletions cmd/outline-ss-server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,24 @@ import (
type ServiceConfig struct {
Listeners []ListenerConfig
Keys []KeyConfig
Dialer DialerConfig
}

type ListenerType string

const listenerTypeTCP ListenerType = "tcp"

const listenerTypeUDP ListenerType = "udp"

type ListenerConfig struct {
Type ListenerType
Address string
}

type DialerConfig struct {
Fwmark uint
}

type KeyConfig struct {
ID string
Cipher string
Expand Down
23 changes: 13 additions & 10 deletions cmd/outline-ss-server/config_example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,22 @@ services:
- type: udp
address: "[::]:9000"
keys:
- id: user-0
cipher: chacha20-ietf-poly1305
secret: Secret0
- id: user-1
cipher: chacha20-ietf-poly1305
secret: Secret1

- id: user-0
cipher: chacha20-ietf-poly1305
secret: Secret0
- id: user-1
cipher: chacha20-ietf-poly1305
secret: Secret1
dialer:
# 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)
fwmark: 0
- listeners:
- type: tcp
address: "[::]:9001"
- type: udp
address: "[::]:9001"
keys:
- id: user-2
cipher: chacha20-ietf-poly1305
secret: Secret2
- id: user-2
cipher: chacha20-ietf-poly1305
secret: Secret2
34 changes: 25 additions & 9 deletions cmd/outline-ss-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,21 @@ import (
"time"

"github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks"
"github.com/Jigsaw-Code/outline-ss-server/ipinfo"
outline_prometheus "github.com/Jigsaw-Code/outline-ss-server/prometheus"
"github.com/Jigsaw-Code/outline-ss-server/service"
"github.com/lmittmann/tint"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"golang.org/x/term"

"github.com/Jigsaw-Code/outline-ss-server/ipinfo"
onet "github.com/Jigsaw-Code/outline-ss-server/net"
outline_prometheus "github.com/Jigsaw-Code/outline-ss-server/prometheus"
"github.com/Jigsaw-Code/outline-ss-server/service"
)

var logLevel = new(slog.LevelVar) // Info by default
var logHandler slog.Handler
var (
logLevel = new(slog.LevelVar) // Info by default
logHandler slog.Handler
)

// Set by goreleaser default ldflags. See https://goreleaser.com/customization/build/
var version = "dev"
Expand Down Expand Up @@ -221,9 +225,10 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) {

ssService, err := service.NewShadowsocksService(
service.WithCiphers(ciphers),
service.WithNatTimeout(s.natTimeout),
service.WithMetrics(s.serviceMetrics),
service.WithReplayCache(&s.replayCache),
service.WithStreamDialer(service.MakeValidatingTCPStreamDialer(onet.RequirePublicIP, 0)),
service.WithPacketListener(service.MakeTargetUDPListener(s.natTimeout, 0)),
service.WithLogger(slog.Default()),
)
ln, err := lnSet.ListenStream(addr)
Expand All @@ -248,9 +253,10 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) {
}
ssService, err := service.NewShadowsocksService(
service.WithCiphers(ciphers),
service.WithNatTimeout(s.natTimeout),
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.WithLogger(slog.Default()),
)
if err != nil {
Expand All @@ -263,14 +269,24 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) {
if err != nil {
return err
}
slog.Info("TCP service started.", "address", ln.Addr().String())
slog.Info("TCP service started.", "address", ln.Addr().String(), "fwmark", func() any {
if serviceConfig.Dialer.Fwmark == 0 {
return "disabled"
}
return serviceConfig.Dialer.Fwmark
}())
go service.StreamServe(ln.AcceptStream, ssService.HandleStream)
case listenerTypeUDP:
pc, err := lnSet.ListenPacket(lnConfig.Address)
if err != nil {
return err
}
slog.Info("UDP service started.", "address", pc.LocalAddr().String())
slog.Info("UDP service started.", "address", pc.LocalAddr().String(), "fwmark", func() any {
if serviceConfig.Dialer.Fwmark == 0 {
return "disabled"
}
return serviceConfig.Dialer.Fwmark
}())
go service.PacketServe(pc, ssService.NewAssociation, s.serverMetrics)
}
}
Expand Down
6 changes: 3 additions & 3 deletions internal/integration_test/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ func TestUDPEcho(t *testing.T) {
if err != nil {
t.Fatal(err)
}
proxy := service.NewPacketHandler(time.Hour, cipherList, &fakeShadowsocksMetrics{})
proxy := service.NewPacketHandler(cipherList, &fakeShadowsocksMetrics{})

proxy.SetTargetIPValidator(allowAll)
natMetrics := &natTestMetrics{}
Expand Down Expand Up @@ -545,7 +545,7 @@ func BenchmarkUDPEcho(b *testing.B) {
if err != nil {
b.Fatal(err)
}
proxy := service.NewPacketHandler(time.Hour, cipherList, &fakeShadowsocksMetrics{})
proxy := service.NewPacketHandler(cipherList, &fakeShadowsocksMetrics{})
proxy.SetTargetIPValidator(allowAll)
done := make(chan struct{})
go func() {
Expand Down Expand Up @@ -591,7 +591,7 @@ func BenchmarkUDPManyKeys(b *testing.B) {
if err != nil {
b.Fatal(err)
}
proxy := service.NewPacketHandler(time.Hour, cipherList, &fakeShadowsocksMetrics{})
proxy := service.NewPacketHandler(cipherList, &fakeShadowsocksMetrics{})
proxy.SetTargetIPValidator(allowAll)
done := make(chan struct{})
go func() {
Expand Down
48 changes: 29 additions & 19 deletions service/shadowsocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,13 @@ import (
"time"

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

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

const (
// 59 seconds is most common timeout for servers that do not respond to invalid requests
tcpReadTimeout time.Duration = 59 * time.Second

// A UDP NAT timeout of at least 5 minutes is recommended in RFC 4787 Section 4.3.
defaultNatTimeout time.Duration = 5 * time.Minute
)

// ShadowsocksConnMetrics is used to report Shadowsocks related metrics on connections.
Expand All @@ -51,14 +50,16 @@ type Service interface {
type Option func(s *ssService)

type ssService struct {
logger *slog.Logger
metrics ServiceMetrics
ciphers CipherList
natTimeout time.Duration
replayCache *ReplayCache

sh StreamHandler
ph PacketHandler
logger *slog.Logger
metrics ServiceMetrics
ciphers CipherList
targetIPValidator onet.TargetIPValidator
replayCache *ReplayCache

streamDialer transport.StreamDialer
sh StreamHandler
packetListener transport.PacketListener
ph PacketHandler
}

// NewShadowsocksService creates a new Shadowsocks service.
Expand All @@ -69,10 +70,6 @@ func NewShadowsocksService(opts ...Option) (Service, error) {
opt(s)
}

// If no NAT timeout is provided via options, use the recommended default.
if s.natTimeout == 0 {
s.natTimeout = defaultNatTimeout
}
// If no logger is provided via options, use a noop logger.
if s.logger == nil {
s.logger = noopLogger()
Expand All @@ -83,9 +80,15 @@ func NewShadowsocksService(opts ...Option) (Service, error) {
NewShadowsocksStreamAuthenticator(s.ciphers, s.replayCache, &ssConnMetrics{ServiceMetrics: s.metrics, proto: "tcp"}, s.logger),
tcpReadTimeout,
)
if s.streamDialer != nil {
s.sh.SetTargetDialer(s.streamDialer)
}
s.sh.SetLogger(s.logger)

s.ph = NewPacketHandler(s.natTimeout, s.ciphers, &ssConnMetrics{ServiceMetrics: s.metrics, proto: "udp"})
s.ph = NewPacketHandler(s.ciphers, &ssConnMetrics{ServiceMetrics: s.metrics, proto: "udp"})
if s.packetListener != nil {
s.ph.SetTargetPacketListener(s.packetListener)
}
s.ph.SetLogger(s.logger)

return s, nil
Expand Down Expand Up @@ -120,10 +123,17 @@ func WithReplayCache(replayCache *ReplayCache) Option {
}
}

// WithNatTimeout option function.
func WithNatTimeout(natTimeout time.Duration) Option {
// WithStreamDialer option function.
func WithStreamDialer(dialer transport.StreamDialer) Option {
return func(s *ssService) {
s.streamDialer = dialer
}
}

// WithPacketListener option function.
func WithPacketListener(listener transport.PacketListener) Option {
return func(s *ssService) {
s.natTimeout = natTimeout
s.packetListener = listener
}
}

Expand Down
33 changes: 33 additions & 0 deletions service/socketopts_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2024 Jigsaw Operations LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build linux

package service

import (
"os"
"syscall"
)

func SetFwmark(rc syscall.RawConn, fwmark uint) error {
var err error
rc.Control(func(fd uintptr) {
err = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(fwmark))
})
if err != nil {
return os.NewSyscallError("failed to set fwmark for socket", err)
}
return nil
}
17 changes: 5 additions & 12 deletions service/tcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ import (
"net"
"net/netip"
"sync"
"syscall"
"time"

"github.com/Jigsaw-Code/outline-sdk/transport"
"github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks"
"github.com/shadowsocks/go-shadowsocks2/socks"

onet "github.com/Jigsaw-Code/outline-ss-server/net"
"github.com/Jigsaw-Code/outline-ss-server/service/metrics"
"github.com/shadowsocks/go-shadowsocks2/socks"
)

// TCPConnMetrics is used to report metrics on TCP connections.
Expand Down Expand Up @@ -172,19 +172,10 @@ func NewStreamHandler(authenticate StreamAuthenticateFunc, timeout time.Duration
logger: noopLogger(),
readTimeout: timeout,
authenticate: authenticate,
dialer: defaultDialer,
dialer: MakeValidatingTCPStreamDialer(onet.RequirePublicIP, 0),
}
}

var defaultDialer = makeValidatingTCPStreamDialer(onet.RequirePublicIP)

func makeValidatingTCPStreamDialer(targetIPValidator onet.TargetIPValidator) transport.StreamDialer {
return &transport.TCPDialer{Dialer: net.Dialer{Control: func(network, address string, c syscall.RawConn) error {
ip, _, _ := net.SplitHostPort(address)
return targetIPValidator(net.ParseIP(ip))
}}}
}

// StreamHandler is a handler that handles stream connections.
type StreamHandler interface {
Handle(ctx context.Context, conn transport.StreamConn, connMetrics TCPConnMetrics)
Expand Down Expand Up @@ -399,6 +390,8 @@ type NoOpTCPConnMetrics struct{}
var _ TCPConnMetrics = (*NoOpTCPConnMetrics)(nil)

func (m *NoOpTCPConnMetrics) AddAuthenticated(accessKey string) {}

func (m *NoOpTCPConnMetrics) AddClosed(status string, data metrics.ProxyMetrics, duration time.Duration) {
}

func (m *NoOpTCPConnMetrics) AddProbe(status, drainResult string, clientProxyBytes int64) {}
40 changes: 40 additions & 0 deletions service/tcp_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright 2024 Jigsaw Operations LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build linux

package service

import (
"net"
"syscall"

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

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

// 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)
func MakeValidatingTCPStreamDialer(targetIPValidator onet.TargetIPValidator, fwmark uint) transport.StreamDialer {
return &transport.TCPDialer{Dialer: net.Dialer{Control: func(network, address string, c syscall.RawConn) error {
if fwmark > 0 {
if err := SetFwmark(c, fwmark); err != nil {
return err
}
}
ip, _, _ := net.SplitHostPort(address)
return targetIPValidator(net.ParseIP(ip))
}}}
}
Loading

0 comments on commit d14ea20

Please sign in to comment.