Skip to content

Commit 98db5b4

Browse files
sabifysbruens
andauthored
feat: enable fwmark (SO_MARK) for outgoing sockets (#202)
* 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]>
1 parent ff61c9f commit 98db5b4

13 files changed

+314
-48
lines changed

cmd/outline-ss-server/config.go

+6
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,24 @@ import (
2424
type ServiceConfig struct {
2525
Listeners []ListenerConfig
2626
Keys []KeyConfig
27+
Dialer DialerConfig
2728
}
2829

2930
type ListenerType string
3031

3132
const listenerTypeTCP ListenerType = "tcp"
33+
3234
const listenerTypeUDP ListenerType = "udp"
3335

3436
type ListenerConfig struct {
3537
Type ListenerType
3638
Address string
3739
}
3840

41+
type DialerConfig struct {
42+
Fwmark uint
43+
}
44+
3945
type KeyConfig struct {
4046
ID string
4147
Cipher string

cmd/outline-ss-server/config_example.yml

+13-10
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,22 @@ services:
2121
- type: udp
2222
address: "[::]:9000"
2323
keys:
24-
- id: user-0
25-
cipher: chacha20-ietf-poly1305
26-
secret: Secret0
27-
- id: user-1
28-
cipher: chacha20-ietf-poly1305
29-
secret: Secret1
30-
24+
- id: user-0
25+
cipher: chacha20-ietf-poly1305
26+
secret: Secret0
27+
- id: user-1
28+
cipher: chacha20-ietf-poly1305
29+
secret: Secret1
30+
dialer:
31+
# fwmark can be used in conjunction with other Linux networking features like cgroups, network namespaces, and TC (Traffic Control) for sophisticated network management.
32+
# Value of 0 disables fwmark (SO_MARK) (Linux Only)
33+
fwmark: 0
3134
- listeners:
3235
- type: tcp
3336
address: "[::]:9001"
3437
- type: udp
3538
address: "[::]:9001"
3639
keys:
37-
- id: user-2
38-
cipher: chacha20-ietf-poly1305
39-
secret: Secret2
40+
- id: user-2
41+
cipher: chacha20-ietf-poly1305
42+
secret: Secret2

cmd/outline-ss-server/main.go

+23-7
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,21 @@ import (
2828
"time"
2929

3030
"github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks"
31-
"github.com/Jigsaw-Code/outline-ss-server/ipinfo"
32-
outline_prometheus "github.com/Jigsaw-Code/outline-ss-server/prometheus"
33-
"github.com/Jigsaw-Code/outline-ss-server/service"
3431
"github.com/lmittmann/tint"
3532
"github.com/prometheus/client_golang/prometheus"
3633
"github.com/prometheus/client_golang/prometheus/promhttp"
3734
"golang.org/x/term"
35+
36+
"github.com/Jigsaw-Code/outline-ss-server/ipinfo"
37+
onet "github.com/Jigsaw-Code/outline-ss-server/net"
38+
outline_prometheus "github.com/Jigsaw-Code/outline-ss-server/prometheus"
39+
"github.com/Jigsaw-Code/outline-ss-server/service"
3840
)
3941

40-
var logLevel = new(slog.LevelVar) // Info by default
41-
var logHandler slog.Handler
42+
var (
43+
logLevel = new(slog.LevelVar) // Info by default
44+
logHandler slog.Handler
45+
)
4246

4347
// Set by goreleaser default ldflags. See https://goreleaser.com/customization/build/
4448
var version = "dev"
@@ -251,6 +255,8 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) {
251255
service.WithNatTimeout(s.natTimeout),
252256
service.WithMetrics(s.serviceMetrics),
253257
service.WithReplayCache(&s.replayCache),
258+
service.WithStreamDialer(service.MakeValidatingTCPStreamDialer(onet.RequirePublicIP, serviceConfig.Dialer.Fwmark)),
259+
service.WithPacketListener(service.MakeTargetUDPListener(serviceConfig.Dialer.Fwmark)),
254260
service.WithLogger(slog.Default()),
255261
)
256262
if err != nil {
@@ -263,14 +269,24 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) {
263269
if err != nil {
264270
return err
265271
}
266-
slog.Info("TCP service started.", "address", ln.Addr().String())
272+
slog.Info("TCP service started.", "address", ln.Addr().String(), "fwmark", func() any {
273+
if serviceConfig.Dialer.Fwmark == 0 {
274+
return "disabled"
275+
}
276+
return serviceConfig.Dialer.Fwmark
277+
}())
267278
go service.StreamServe(ln.AcceptStream, ssService.HandleStream)
268279
case listenerTypeUDP:
269280
pc, err := lnSet.ListenPacket(lnConfig.Address)
270281
if err != nil {
271282
return err
272283
}
273-
slog.Info("UDP service started.", "address", pc.LocalAddr().String())
284+
slog.Info("UDP service started.", "address", pc.LocalAddr().String(), "fwmark", func() any {
285+
if serviceConfig.Dialer.Fwmark == 0 {
286+
return "disabled"
287+
}
288+
return serviceConfig.Dialer.Fwmark
289+
}())
274290
go ssService.HandlePacket(pc)
275291
}
276292
}

service/shadowsocks.go

+33-8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import (
2121
"time"
2222

2323
"github.com/Jigsaw-Code/outline-sdk/transport"
24+
25+
onet "github.com/Jigsaw-Code/outline-ss-server/net"
2426
)
2527

2628
const (
@@ -51,14 +53,17 @@ type Service interface {
5153
type Option func(s *ssService)
5254

5355
type ssService struct {
54-
logger *slog.Logger
55-
metrics ServiceMetrics
56-
ciphers CipherList
57-
natTimeout time.Duration
58-
replayCache *ReplayCache
59-
60-
sh StreamHandler
61-
ph PacketHandler
56+
logger *slog.Logger
57+
metrics ServiceMetrics
58+
ciphers CipherList
59+
natTimeout time.Duration
60+
targetIPValidator onet.TargetIPValidator
61+
replayCache *ReplayCache
62+
63+
streamDialer transport.StreamDialer
64+
sh StreamHandler
65+
packetListener transport.PacketListener
66+
ph PacketHandler
6267
}
6368

6469
// NewShadowsocksService creates a new Shadowsocks service.
@@ -83,9 +88,15 @@ func NewShadowsocksService(opts ...Option) (Service, error) {
8388
NewShadowsocksStreamAuthenticator(s.ciphers, s.replayCache, &ssConnMetrics{ServiceMetrics: s.metrics, proto: "tcp"}, s.logger),
8489
tcpReadTimeout,
8590
)
91+
if s.streamDialer != nil {
92+
s.sh.SetTargetDialer(s.streamDialer)
93+
}
8694
s.sh.SetLogger(s.logger)
8795

8896
s.ph = NewPacketHandler(s.natTimeout, s.ciphers, s.metrics, &ssConnMetrics{ServiceMetrics: s.metrics, proto: "udp"})
97+
if s.packetListener != nil {
98+
s.ph.SetTargetPacketListener(s.packetListener)
99+
}
89100
s.ph.SetLogger(s.logger)
90101

91102
return s, nil
@@ -127,6 +138,20 @@ func WithNatTimeout(natTimeout time.Duration) Option {
127138
}
128139
}
129140

141+
// WithStreamDialer option function.
142+
func WithStreamDialer(dialer transport.StreamDialer) Option {
143+
return func(s *ssService) {
144+
s.streamDialer = dialer
145+
}
146+
}
147+
148+
// WithPacketListener option function.
149+
func WithPacketListener(listener transport.PacketListener) Option {
150+
return func(s *ssService) {
151+
s.packetListener = listener
152+
}
153+
}
154+
130155
// HandleStream handles a Shadowsocks stream-based connection.
131156
func (s *ssService) HandleStream(ctx context.Context, conn transport.StreamConn) {
132157
var connMetrics TCPConnMetrics

service/socketopts_linux.go

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright 2024 Jigsaw Operations LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//go:build linux
16+
17+
package service
18+
19+
import (
20+
"os"
21+
"syscall"
22+
)
23+
24+
func SetFwmark(rc syscall.RawConn, fwmark uint) error {
25+
var err error
26+
rc.Control(func(fd uintptr) {
27+
err = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(fwmark))
28+
})
29+
if err != nil {
30+
return os.NewSyscallError("failed to set fwmark for socket", err)
31+
}
32+
return nil
33+
}

service/tcp.go

+5-12
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ import (
2525
"net"
2626
"net/netip"
2727
"sync"
28-
"syscall"
2928
"time"
3029

3130
"github.com/Jigsaw-Code/outline-sdk/transport"
3231
"github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks"
32+
"github.com/shadowsocks/go-shadowsocks2/socks"
33+
3334
onet "github.com/Jigsaw-Code/outline-ss-server/net"
3435
"github.com/Jigsaw-Code/outline-ss-server/service/metrics"
35-
"github.com/shadowsocks/go-shadowsocks2/socks"
3636
)
3737

3838
// TCPConnMetrics is used to report metrics on TCP connections.
@@ -170,19 +170,10 @@ func NewStreamHandler(authenticate StreamAuthenticateFunc, timeout time.Duration
170170
logger: noopLogger(),
171171
readTimeout: timeout,
172172
authenticate: authenticate,
173-
dialer: defaultDialer,
173+
dialer: MakeValidatingTCPStreamDialer(onet.RequirePublicIP, 0),
174174
}
175175
}
176176

177-
var defaultDialer = makeValidatingTCPStreamDialer(onet.RequirePublicIP)
178-
179-
func makeValidatingTCPStreamDialer(targetIPValidator onet.TargetIPValidator) transport.StreamDialer {
180-
return &transport.TCPDialer{Dialer: net.Dialer{Control: func(network, address string, c syscall.RawConn) error {
181-
ip, _, _ := net.SplitHostPort(address)
182-
return targetIPValidator(net.ParseIP(ip))
183-
}}}
184-
}
185-
186177
// StreamHandler is a handler that handles stream connections.
187178
type StreamHandler interface {
188179
Handle(ctx context.Context, conn transport.StreamConn, connMetrics TCPConnMetrics)
@@ -397,6 +388,8 @@ type NoOpTCPConnMetrics struct{}
397388
var _ TCPConnMetrics = (*NoOpTCPConnMetrics)(nil)
398389

399390
func (m *NoOpTCPConnMetrics) AddAuthenticated(accessKey string) {}
391+
400392
func (m *NoOpTCPConnMetrics) AddClosed(status string, data metrics.ProxyMetrics, duration time.Duration) {
401393
}
394+
402395
func (m *NoOpTCPConnMetrics) AddProbe(status, drainResult string, clientProxyBytes int64) {}

service/tcp_linux.go

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2024 Jigsaw Operations LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//go:build linux
16+
17+
package service
18+
19+
import (
20+
"net"
21+
"syscall"
22+
23+
"github.com/Jigsaw-Code/outline-sdk/transport"
24+
25+
onet "github.com/Jigsaw-Code/outline-ss-server/net"
26+
)
27+
28+
// fwmark can be used in conjunction with other Linux networking features like cgroups, network namespaces, and TC (Traffic Control) for sophisticated network management.
29+
// Value of 0 disables fwmark (SO_MARK) (Linux Only)
30+
func MakeValidatingTCPStreamDialer(targetIPValidator onet.TargetIPValidator, fwmark uint) transport.StreamDialer {
31+
return &transport.TCPDialer{Dialer: net.Dialer{Control: func(network, address string, c syscall.RawConn) error {
32+
if fwmark > 0 {
33+
if err := SetFwmark(c, fwmark); err != nil {
34+
return err
35+
}
36+
}
37+
ip, _, _ := net.SplitHostPort(address)
38+
return targetIPValidator(net.ParseIP(ip))
39+
}}}
40+
}

service/tcp_other.go

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright 2024 Jigsaw Operations LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//go:build !linux
16+
17+
package service
18+
19+
import (
20+
"net"
21+
"syscall"
22+
23+
"github.com/Jigsaw-Code/outline-sdk/transport"
24+
25+
onet "github.com/Jigsaw-Code/outline-ss-server/net"
26+
)
27+
28+
// fwmark can be used in conjunction with other Linux networking features like cgroups, network namespaces, and TC (Traffic Control) for sophisticated network management.
29+
// Value of 0 disables fwmark (SO_MARK) (Linux Only)
30+
func MakeValidatingTCPStreamDialer(targetIPValidator onet.TargetIPValidator, fwmark uint) transport.StreamDialer {
31+
if fwmark != 0 {
32+
panic("fwmark is linux-specific feature and should be 0")
33+
}
34+
return &transport.TCPDialer{Dialer: net.Dialer{Control: func(network, address string, c syscall.RawConn) error {
35+
ip, _, _ := net.SplitHostPort(address)
36+
return targetIPValidator(net.ParseIP(ip))
37+
}}}
38+
}

0 commit comments

Comments
 (0)