Skip to content

Commit

Permalink
feat: enable fwmark (SO_MARK) for outgoing sockets (#202)
Browse files Browse the repository at this point in the history
* feat: enable fwmark (SO_MARK) for outgoing sockets

* fix: make fwmark linux-specific functionality

* fix: minor improvements over handling fwmark

* Use `transport.PacketListener` as interface.

* Take the `syscall.RawConn` as input to `SetFwdmark()`.

* Some cleanup.

* Fix copyright dates for new files.

* Fix the error types.

* Revert changes to integration test.

---------

Co-authored-by: sbruens <[email protected]>
  • Loading branch information
sabify and sbruens authored Dec 16, 2024
1 parent ff61c9f commit 98db5b4
Show file tree
Hide file tree
Showing 13 changed files with 314 additions and 48 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
30 changes: 23 additions & 7 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 @@ -251,6 +255,8 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) {
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(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 ssService.HandlePacket(pc)
}
}
Expand Down
41 changes: 33 additions & 8 deletions service/shadowsocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
"time"

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

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

const (
Expand Down Expand Up @@ -51,14 +53,17 @@ 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
natTimeout time.Duration
targetIPValidator onet.TargetIPValidator
replayCache *ReplayCache

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

// NewShadowsocksService creates a new Shadowsocks service.
Expand All @@ -83,9 +88,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, s.metrics, &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 @@ -127,6 +138,20 @@ 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.packetListener = listener
}
}

// HandleStream handles a Shadowsocks stream-based connection.
func (s *ssService) HandleStream(ctx context.Context, conn transport.StreamConn) {
var connMetrics TCPConnMetrics
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 @@ -170,19 +170,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 @@ -397,6 +388,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))
}}}
}
38 changes: 38 additions & 0 deletions service/tcp_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// 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 {
if fwmark != 0 {
panic("fwmark is linux-specific feature and should be 0")
}
return &transport.TCPDialer{Dialer: net.Dialer{Control: func(network, address string, c syscall.RawConn) error {
ip, _, _ := net.SplitHostPort(address)
return targetIPValidator(net.ParseIP(ip))
}}}
}
Loading

0 comments on commit 98db5b4

Please sign in to comment.