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

feat: enable fwmark (SO_MARK) for outgoing sockets #202

Merged
merged 10 commits into from
Dec 16, 2024
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 {
sabify marked this conversation as resolved.
Show resolved Hide resolved
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
Loading