Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ Easyss 通过检测配置文件自动区分模式:

在 `easyss` 所在目录下新建文本文件(如 `direct.txt`、`proxy.txt`),IP/CIDR/域名可混写,每行一条记录。然后在配置中指定路径:

> 配置中的相对路径(`direct_file`、`proxy_file`、`ca_path` 等)会先按当前工作目录查找,找不到时自动回退到 `easyss` 可执行文件所在目录(macOS 下为 `.app` 旁)。这样从 Finder 双击、开机自启(launchd)等方式启动时也能正常读取,不受启动目录影响。

**简化模式:**

```json
Expand Down
16 changes: 15 additions & 1 deletion client/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
utls "github.com/refraction-networking/utls"

"github.com/nange/easyss/v3/config"
"github.com/nange/easyss/v3/util"
)

// DirectDNSServers are the public DNS servers used for direct (non-proxied) DNS lookups.
Expand Down Expand Up @@ -56,7 +57,7 @@ type TransportConfig struct {
StreamThreshold int `json:"stream_threshold"`
PrioritySlotRatio float64 `json:"priority_slot_ratio"`
ConnLifetimeSec int `json:"conn_lifetime_sec"` // max connection lifetime in seconds, 0 uses default
ConnMaxBytes int64 `json:"conn_max_bytes"` // max bytes per connection, 0 uses default
ConnMaxBytes int64 `json:"conn_max_bytes"` // max bytes carried by a connection in either direction, 0 uses default
}

type ShaperConfig struct {
Expand Down Expand Up @@ -222,6 +223,19 @@ func applyDefaults(c *ClientConfig) {
}
}

// ResolveFilePaths resolves relative file paths in the config against the
// executable directory when they cannot be found in the current working
// directory. On macOS the app is often launched by Finder/launchd with cwd=/,
// so relative paths like direct.txt, proxy.txt or ca_path would otherwise not
// be found even though the files sit next to the binary/.app bundle.
func (c *ClientConfig) ResolveFilePaths() {
c.Routing.DirectFile = util.ResolvePath(c.Routing.DirectFile)
c.Routing.ProxyFile = util.ResolvePath(c.Routing.ProxyFile)
for _, srv := range c.Servers {
srv.CAPath = util.ResolvePath(srv.CAPath)
}
}

func (c *ClientConfig) Clone() *ClientConfig {
data, err := json.Marshal(c)
if err != nil {
Expand Down
36 changes: 36 additions & 0 deletions client/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"

"github.com/nange/easyss/v3/config"
"github.com/nange/easyss/v3/util"
)

func TestDefaultConfig(t *testing.T) {
Expand Down Expand Up @@ -640,3 +641,38 @@ func TestApplyDefaults(t *testing.T) {
}
})
}

func TestResolveFilePaths(t *testing.T) {
relDirect := "direct.txt"
relCA := "ca.pem"
abs, err := filepath.Abs("proxy.txt")
if err != nil {
t.Fatal(err)
}

cfg := &ClientConfig{
Routing: RoutingConfig{
DirectFile: relDirect,
ProxyFile: abs,
},
Servers: []*ServerProfile{
{CAPath: relCA},
{CAPath: ""},
},
}

cfg.ResolveFilePaths()

if want := filepath.Join(util.CurrentDir(), relDirect); cfg.Routing.DirectFile != want {
t.Errorf("DirectFile = %q, want %q", cfg.Routing.DirectFile, want)
}
if cfg.Routing.ProxyFile != abs {
t.Errorf("ProxyFile = %q, want %q (absolute unchanged)", cfg.Routing.ProxyFile, abs)
}
if want := filepath.Join(util.CurrentDir(), relCA); cfg.Servers[0].CAPath != want {
t.Errorf("Servers[0].CAPath = %q, want %q", cfg.Servers[0].CAPath, want)
}
if cfg.Servers[1].CAPath != "" {
t.Errorf("Servers[1].CAPath = %q, want empty (unchanged)", cfg.Servers[1].CAPath)
}
}
10 changes: 10 additions & 0 deletions cmd/easyss-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ func main() {
os.Exit(0)
}

// On macOS the server is often launched by launchd with cwd=/,
// so a relative config path is first looked up in the cwd and then
// falls back to the executable directory.
configFile = util.ResolvePath(configFile)

data, err := os.ReadFile(configFile)
if err != nil {
log.Error("[EASYSS-SERVER-V3] read config", "err", err)
Expand All @@ -55,6 +60,11 @@ func main() {
log.Error("[EASYSS-SERVER-V3] parse config", "err", err)
os.Exit(1)
}
// Resolve relative file paths (cert_path/key_path/next_proxy_file)
// against the executable directory so that macOS launchd launches
// (cwd=/) can still find the files placed next to the binary.
// Must run before EffectiveServerConfig, which copies by value.
fileCfg.ResolveFilePaths()
cfg = fileCfg.EffectiveServerConfig()
if pprofEnabled {
cfg.PprofEnabled = true
Expand Down
19 changes: 9 additions & 10 deletions cmd/easyss/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,10 @@ func main() {
os.Exit(runTunHelper(tunHTTPAddr, tunFDSocket, logFile, sc.LogLevel))
}

if !filepath.IsAbs(configFile) {
if _, err := os.Stat(configFile); os.IsNotExist(err) {
if dir := util.CurrentDir(); dir != "" {
altPath := filepath.Join(dir, configFile)
if _, err := os.Stat(altPath); err == nil {
configFile = altPath
}
}
}
}
// On macOS the app is often launched by Finder/launchd with cwd=/,
// so a relative config path is first looked up in the cwd and then
// falls back to the executable directory.
configFile = util.ResolvePath(configFile)

cfg, err := config.LoadConfig(configFile)
if err != nil {
Expand All @@ -115,6 +109,11 @@ func main() {
config.ApplySimpleOverrides(cfg, sc)
}

// Resolve relative file paths (direct_file/proxy_file/ca_path) against
// the executable directory so that macOS Finder/launchd launches (cwd=/)
// can still find the files placed next to the binary/.app bundle.
cfg.ResolveFilePaths()

if cfg.Log.FilePath != "" && !filepath.IsAbs(cfg.Log.FilePath) {
if dir := util.CurrentDir(); dir != "" {
cfg.Log.FilePath = filepath.Join(dir, cfg.Log.FilePath)
Expand Down
14 changes: 10 additions & 4 deletions config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,17 @@ const (
// either limit stops accepting new streams and its idle connection is
// closed, so the next stream dials a fresh one (invisible to users).
DefaultConnLifetimeSec = 900 // 15min
DefaultConnMaxBytes = 150 * 1024 * 1024 // 150MB
DefaultConnMaxBytes = 150 * 1024 * 1024 // 150MB,双向(上下行)累计

HTTP2ServerMaxReadFrameSize = 1<<24 - 1 // 16MB-1,nginx/Cloudflare 主流值
HTTP2ServerReceiveBufferPerConnection = 1 << 20 // 1MB,避免 64KB 瓶颈导致长期运行吞吐量下降
HTTP2ServerReceiveBufferPerStream = 256 * 1024 // 256KB,流级别接收窗口
// Upload flow control on the server side: the per-stream window bounds
// a single upload stream's in-flight data, capping its throughput at
// roughly window/RTT. 256KB would pin a single-stream upload to
// ~6.8Mbps on a 300ms link; 1MB (the stdlib server default) raises that
// to ~26Mbps, and the 4MB connection window keeps aggregate uploads
// from being constrained to ~1MB in flight per connection.
HTTP2ServerMaxReadFrameSize = 1<<24 - 1 // 16MB-1,nginx/Cloudflare 主流值
HTTP2ServerReceiveBufferPerConnection = 4 << 20 // 4MB,连接级上行窗口
HTTP2ServerReceiveBufferPerStream = 1 << 20 // 1MB,流级上行窗口(stdlib 服务端默认)

HTTP2ClientMaxReadFrameSize = 1 * 1024 * 1024 // 1MB,Chrome MAX_FRAME_SIZE
HTTP2ClientReceiveBufferPerConnection = 15 * 1024 * 1024 // ~15MB,Chrome 连接级窗口
Expand Down
15 changes: 15 additions & 0 deletions server/config/config.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package config

import "github.com/nange/easyss/v3/util"

type LogConfig struct {
Level string `json:"level"`
FilePath string `json:"file_path"`
Expand Down Expand Up @@ -51,6 +53,19 @@ func (fc *FileConfig) EffectiveServerConfig() ServerConfig {
return cfg
}

// ResolveFilePaths resolves relative file paths in the config against the
// executable directory when they cannot be found in the current working
// directory. Must be called before EffectiveServerConfig since the latter
// copies the Server/NextProxy structs by value. On macOS the server is often
// launched by launchd with cwd=/, so relative paths like cert_path/key_path
// or next_proxy_file would otherwise not be found even though the files sit
// next to the binary.
func (fc *FileConfig) ResolveFilePaths() {
fc.Server.CertPath = util.ResolvePath(fc.Server.CertPath)
fc.Server.KeyPath = util.ResolvePath(fc.Server.KeyPath)
fc.NextProxy.NextProxyFile = util.ResolvePath(fc.NextProxy.NextProxyFile)
}

func (c *ServerConfig) GetAllowedMethods() []string {
if len(c.AllowedMethods) == 0 {
return []string{"aes-256-gcm", "chacha20-poly1305"}
Expand Down
69 changes: 69 additions & 0 deletions server/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package config

import (
"encoding/json"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"

"github.com/nange/easyss/v3/util"
)

func TestFileConfigEffectiveServerConfig(t *testing.T) {
Expand Down Expand Up @@ -33,3 +36,69 @@ func TestFileConfigEffectiveServerConfig(t *testing.T) {
require.Equal(t, "socks5://127.0.0.1:1080", cfg.NextProxy.URL)
require.True(t, cfg.NextProxy.EnableUDP)
}

func TestResolveFilePaths(t *testing.T) {
relCert := "server.crt"
relNextProxy := "next_proxy.txt"
abs, err := filepath.Abs("server.key")
if err != nil {
t.Fatal(err)
}

fc := &FileConfig{
Server: ServerConfig{
CertPath: relCert,
KeyPath: abs,
},
NextProxy: NextProxyConfig{
NextProxyFile: relNextProxy,
},
}

fc.ResolveFilePaths()

if want := filepath.Join(util.CurrentDir(), relCert); fc.Server.CertPath != want {
t.Errorf("CertPath = %q, want %q", fc.Server.CertPath, want)
}
if fc.Server.KeyPath != abs {
t.Errorf("KeyPath = %q, want %q (absolute unchanged)", fc.Server.KeyPath, abs)
}
if want := filepath.Join(util.CurrentDir(), relNextProxy); fc.NextProxy.NextProxyFile != want {
t.Errorf("NextProxyFile = %q, want %q", fc.NextProxy.NextProxyFile, want)
}
}

func TestResolveFilePathsEmpty(t *testing.T) {
fc := &FileConfig{}
fc.ResolveFilePaths()

if fc.Server.CertPath != "" || fc.Server.KeyPath != "" || fc.NextProxy.NextProxyFile != "" {
t.Errorf("empty paths should stay empty, got %+v", fc)
}
}

func TestEffectiveServerConfigCarriesResolvedPaths(t *testing.T) {
fc := &FileConfig{
Server: ServerConfig{
CertPath: "server.crt",
KeyPath: "server.key",
},
NextProxy: NextProxyConfig{
NextProxyFile: "next_proxy.txt",
},
Timeout: 30,
}

fc.ResolveFilePaths()
cfg := fc.EffectiveServerConfig()

if cfg.CertPath != fc.Server.CertPath {
t.Errorf("effective CertPath = %q, want %q", cfg.CertPath, fc.Server.CertPath)
}
if cfg.NextProxy.NextProxyFile != fc.NextProxy.NextProxyFile {
t.Errorf("effective NextProxyFile = %q, want %q", cfg.NextProxy.NextProxyFile, fc.NextProxy.NextProxyFile)
}
if cfg.Timeout != 30 {
t.Errorf("Timeout = %d, want 30", cfg.Timeout)
}
}
29 changes: 19 additions & 10 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,10 +306,23 @@ func (s *Server) Start() error {
s.mux.Handle(sharedconfig.EndpointUDP, proxyHandler)
s.mux.Handle(sharedconfig.EndpointICMP, proxyHandler)

s.httpServer = &http.Server{
Addr: s.cfg.Listen,
s.httpServer = buildHTTPServer(cfg, tlsConfig, s.mux, timeout)

log.Info("[SERVER] listening", "addr", s.cfg.Listen, "routes", []string{"/", sharedconfig.EndpointTCP, sharedconfig.EndpointUDP, sharedconfig.EndpointICMP})
s.statsDone = make(chan struct{})
go s.statsLoop()
return s.httpServer.ListenAndServeTLS("", "")
}

// buildHTTPServer assembles the HTTP server with HTTP/2 flow-control windows
// sized for upload throughput: the per-stream receive window bounds a single
// upload stream's in-flight data (throughput ≈ window/RTT), so both windows
// must be generous enough for high-RTT links.
func buildHTTPServer(cfg *config.ServerConfig, tlsConfig *tls.Config, mux *http.ServeMux, timeout time.Duration) *http.Server {
srv := &http.Server{
Addr: cfg.Listen,
TLSConfig: tlsConfig,
Handler: s.mux,
Handler: mux,
ErrorLog: stdErrorLog(),
Protocols: &http.Protocols{},
HTTP2: &http.HTTP2Config{
Expand All @@ -320,13 +333,9 @@ func (s *Server) Start() error {
IdleTimeout: 8 * timeout,
ReadHeaderTimeout: min(timeout/2, 10*time.Second),
}
s.httpServer.Protocols.SetHTTP1(true)
s.httpServer.Protocols.SetHTTP2(true)

log.Info("[SERVER] listening", "addr", s.cfg.Listen, "routes", []string{"/", sharedconfig.EndpointTCP, sharedconfig.EndpointUDP, sharedconfig.EndpointICMP})
s.statsDone = make(chan struct{})
go s.statsLoop()
return s.httpServer.ListenAndServeTLS("", "")
srv.Protocols.SetHTTP1(true)
srv.Protocols.SetHTTP2(true)
return srv
}

func (s *Server) Shutdown(ctx context.Context) error {
Expand Down
Loading
Loading