diff --git a/args.go b/args.go index 88d86b6e..384013af 100644 --- a/args.go +++ b/args.go @@ -138,6 +138,10 @@ func parsePluginOptions(s string) (opts Args, err error) { i++ continue } + if key == "disableAccessLog" { + err = fmt.Errorf("option %q does not accept a value", key) + return + } // Skip the equals sign. i++ // Read the value. diff --git a/docs/CLI.md b/docs/CLI.md index ebd9512e..2d5e5516 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -43,6 +43,7 @@ Shadowsocks server to which it forwards traffic. | `-mux` | `1` | Number of concurrent multiplexed WebSocket connections on the client. Set to `0` to disable WebSocket multiplexing. Ignored by QUIC and Hysteria2. | | `-fwmark` | `0` | Set the outbound socket `SO_MARK` value. `0` disables it; nonzero values must fit in an unsigned 32-bit integer. Platform and kernel support are required. | | `-loglevel` | `warning` | V2Ray log level: `debug`, `info`, `warning`, `error`, or `none`. An empty or unrecognized value uses `warning`. | +| `-disableAccessLog` | `false` | Disable V2Ray access logs. Access logging remains enabled by default. `-loglevel=none` still disables all V2Ray logs. | | `-version` | `false` | Print the plugin version and Go runtime version, then exit. | The table lists every application flag. The Go flag package also provides @@ -119,6 +120,7 @@ same names use the direct CLI meanings described earlier. | `fastOpen` | presence | Enable TCP Fast Open. The option name is camelCase. | | `fwmark` | unsigned integer | Set the outbound socket mark; `0` disables it. | | `loglevel` | `debug`, `info`, `warning`, `error`, `none` | Set the V2Ray log level. | +| `disableAccessLog` | presence only | Disable V2Ray access logs. Do not add `=value`; values are rejected. | | `__android_vpn` | presence | Internal Android VPN socket-protection option. Do not use in normal desktop configurations. | There is no SIP003 option for `-version`. The Android `__android_vpn` option @@ -144,12 +146,19 @@ The parser removes the escape backslashes, so the values above become `/ws;v2` and `secret=part`. For a literal backslash, use `\\` in the option string. A bare boolean key enables the option. Boolean values are based on presence, so `tls=false` still enables TLS; omit `tls` to leave it disabled. +`disableAccessLog` is a presence-only opt-out: use the bare key, and do not +provide a value. For duplicate keys, the first value is used. Unknown keys are ignored. Direct command-line flags are parsed first; complete SIP003 endpoint variables then override matching endpoints, and recognized `SS_PLUGIN_OPTIONS` entries -override matching values. If a plugin option is absent, its command-line value -or default remains in effect. +override matching values. `disableAccessLog` is an opt-out: if the CLI flag is +true or the SIP003 key is present, access logs are disabled. Either source can +request disabling; neither can re-enable logs disabled by the other. If the +key is absent and the CLI flag is false, access logs remain enabled. + +With all SIP003 endpoint variables present, malformed `SS_PLUGIN_OPTIONS` +causes startup to fail with a configuration error. ## Transport modes diff --git a/main.go b/main.go index 8e025270..e2baa87d 100644 --- a/main.go +++ b/main.go @@ -42,25 +42,26 @@ const maxUint32 uint64 = 1<<32 - 1 var ( VERSION = "custom" - vpn = flag.Bool("V", false, "Run in VPN mode.") - fastOpen = flag.Bool("fast-open", false, "Enable TCP fast open.") - localAddr = flag.String("localAddr", "127.0.0.1", "local address to listen on.") - localPort = flag.String("localPort", "1984", "local port to listen on.") - remoteAddr = flag.String("remoteAddr", "127.0.0.1", "remote address to forward.") - remotePort = flag.String("remotePort", "1080", "remote port to forward.") - path = flag.String("path", "/", "URL path for websocket.") - host = flag.String("host", "cloudfront.com", "Hostname for server.") - tlsEnabled = flag.Bool("tls", false, "Enable TLS.") - cert = flag.String("cert", "", "Path to TLS certificate file. Overrides certRaw. Server default: ~/.acme.sh/{host}/fullchain.cer; tries {host}_ecc when cert, certRaw, and key are unset and the default pair is missing.") - certRaw = flag.String("certRaw", "", "Raw TLS certificate content. Intended only for Android.") - key = flag.String("key", "", "(server) Path to TLS key file. Default: ~/.acme.sh/{host}/{host}.key; uses the same directory as the automatically selected certificate.") - mode = flag.String("mode", "websocket", "Transport mode: websocket, quic, hysteria2 (QUIC and Hysteria2 enforce TLS).") - password = flag.String("password", "", "Optional Hysteria2 transport password, separate from the Shadowsocks password.") - mux = flag.Uint("mux", 1, "Concurrent multiplexed connections (websocket client mode only).") - server = flag.Bool("server", false, "Run in server mode") - logLevel = flag.String("loglevel", "", "loglevel for v2ray: debug, info, warning (default), error, none.") - version = flag.Bool("version", false, "Show current version of v2ray-plugin") - fwmark = flag.Uint("fwmark", 0, "Set SO_MARK option for outbound sockets.") + vpn = flag.Bool("V", false, "Run in VPN mode.") + fastOpen = flag.Bool("fast-open", false, "Enable TCP fast open.") + localAddr = flag.String("localAddr", "127.0.0.1", "local address to listen on.") + localPort = flag.String("localPort", "1984", "local port to listen on.") + remoteAddr = flag.String("remoteAddr", "127.0.0.1", "remote address to forward.") + remotePort = flag.String("remotePort", "1080", "remote port to forward.") + path = flag.String("path", "/", "URL path for websocket.") + host = flag.String("host", "cloudfront.com", "Hostname for server.") + tlsEnabled = flag.Bool("tls", false, "Enable TLS.") + cert = flag.String("cert", "", "Path to TLS certificate file. Overrides certRaw. Server default: ~/.acme.sh/{host}/fullchain.cer; tries {host}_ecc when cert, certRaw, and key are unset and the default pair is missing.") + certRaw = flag.String("certRaw", "", "Raw TLS certificate content. Intended only for Android.") + key = flag.String("key", "", "(server) Path to TLS key file. Default: ~/.acme.sh/{host}/{host}.key; uses the same directory as the automatically selected certificate.") + mode = flag.String("mode", "websocket", "Transport mode: websocket, quic, hysteria2 (QUIC and Hysteria2 enforce TLS).") + password = flag.String("password", "", "Optional Hysteria2 transport password, separate from the Shadowsocks password.") + mux = flag.Uint("mux", 1, "Concurrent multiplexed connections (websocket client mode only).") + server = flag.Bool("server", false, "Run in server mode") + logLevel = flag.String("loglevel", "", "loglevel for v2ray: debug, info, warning (default), error, none.") + disableAccessLog = flag.Bool("disableAccessLog", false, "Disable V2Ray access logs.") + version = flag.Bool("version", false, "Show current version of v2ray-plugin") + fwmark = flag.Uint("fwmark", 0, "Set SO_MARK option for outbound sockets.") ) func homeDir() string { @@ -112,10 +113,14 @@ func readACMECertificate(home, host string) (tls.Certificate, error) { return tls.Certificate{}, fmt.Errorf("no acme.sh certificate pair found for %s", host) } -func logConfig(logLevel string) *vlog.Config { +func logConfig(logLevel string, disableAccessLog bool) *vlog.Config { + accessLogType := vlog.LogType_Console + if disableAccessLog { + accessLogType = vlog.LogType_None + } config := &vlog.Config{ Error: &vlog.LogSpecification{Type: vlog.LogType_Console, Level: clog.Severity_Warning}, - Access: &vlog.LogSpecification{Type: vlog.LogType_Console}, + Access: &vlog.LogSpecification{Type: accessLogType}, } level := strings.ToLower(logLevel) switch level { @@ -255,7 +260,7 @@ func generateConfig() (*core.Config, error) { serial.ToTypedMessage(&dispatcher.Config{}), serial.ToTypedMessage(&proxyman.InboundConfig{}), serial.ToTypedMessage(&proxyman.OutboundConfig{}), - serial.ToTypedMessage(logConfig(*logLevel)), + serial.ToTypedMessage(logConfig(*logLevel, *disableAccessLog)), } if *server { @@ -317,93 +322,97 @@ func generateConfig() (*core.Config, error) { func startV2Ray() (core.Server, error) { opts, err := parseEnv() + if err != nil { + return nil, newError("failed to parse plugin options").Base(err) + } - if err == nil { - if c, b := opts.Get("mode"); b { - *mode = c - } - if c, b := opts.Get("password"); b { - *password = c - } - if c, b := opts.Get("mux"); b { - if i, err := strconv.ParseUint(c, 10, strconv.IntSize); err == nil { - *mux = uint(i) - } else { - logWarn("failed to parse mux, use default value") - } - } - if _, b := opts.Get("tls"); b { - *tlsEnabled = true - } - if c, b := opts.Get("host"); b { - *host = c - } - if c, b := opts.Get("path"); b { - *path = c - } - if c, b := opts.Get("cert"); b { - *cert = c - } - if c, b := opts.Get("certRaw"); b { - *certRaw = c - } - if c, b := opts.Get("key"); b { - *key = c - } - if c, b := opts.Get("loglevel"); b { - *logLevel = c - } - if _, b := opts.Get("server"); b { - *server = true + if c, b := opts.Get("mode"); b { + *mode = c + } + if c, b := opts.Get("password"); b { + *password = c + } + if c, b := opts.Get("mux"); b { + if i, err := strconv.ParseUint(c, 10, strconv.IntSize); err == nil { + *mux = uint(i) + } else { + logWarn("failed to parse mux, use default value") } - if c, b := opts.Get("localAddr"); b { - if *server { - *remoteAddr = c - } else { - *localAddr = c - } + } + if _, b := opts.Get("tls"); b { + *tlsEnabled = true + } + if c, b := opts.Get("host"); b { + *host = c + } + if c, b := opts.Get("path"); b { + *path = c + } + if c, b := opts.Get("cert"); b { + *cert = c + } + if c, b := opts.Get("certRaw"); b { + *certRaw = c + } + if c, b := opts.Get("key"); b { + *key = c + } + if c, b := opts.Get("loglevel"); b { + *logLevel = c + } + if _, b := opts.Get("disableAccessLog"); b { + *disableAccessLog = true + } + if _, b := opts.Get("server"); b { + *server = true + } + if c, b := opts.Get("localAddr"); b { + if *server { + *remoteAddr = c + } else { + *localAddr = c } - if c, b := opts.Get("localPort"); b { - if *server { - *remotePort = c - } else { - *localPort = c - } + } + if c, b := opts.Get("localPort"); b { + if *server { + *remotePort = c + } else { + *localPort = c } - if c, b := opts.Get("remoteAddr"); b { - if *server { - *localAddr = c - } else { - *remoteAddr = c - } + } + if c, b := opts.Get("remoteAddr"); b { + if *server { + *localAddr = c + } else { + *remoteAddr = c } - if c, b := opts.Get("remotePort"); b { - if *server { - *localPort = c - } else { - *remotePort = c - } + } + if c, b := opts.Get("remotePort"); b { + if *server { + *localPort = c + } else { + *remotePort = c } + } - if _, b := opts.Get("fastOpen"); b { - *fastOpen = true - } + if _, b := opts.Get("fastOpen"); b { + *fastOpen = true + } - if _, b := opts.Get("__android_vpn"); b { - *vpn = true - } + if _, b := opts.Get("__android_vpn"); b { + *vpn = true + } - if c, b := opts.Get("fwmark"); b { - if i, err := strconv.ParseUint(c, 10, strconv.IntSize); err == nil { - *fwmark = uint(i) - } else { - logWarn("failed to parse fwmark, use default value") - } + if c, b := opts.Get("fwmark"); b { + if i, err := strconv.ParseUint(c, 10, strconv.IntSize); err == nil { + *fwmark = uint(i) + } else { + logWarn("failed to parse fwmark, use default value") } + } - if *vpn { - registerControlFunc() - } + if *vpn { + registerControlFunc() } config, err := generateConfig()