mirror of
https://github.com/go-gost/x.git
synced 2026-10-19 16:16:32 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c06ba17916 | ||
|
|
a32ffd5608 | ||
|
|
ffa7ceaed7 | ||
|
|
ea3ca94e53 | ||
|
|
5d6b00c3a6 | ||
|
|
0637486025 | ||
|
|
99a0f7323d | ||
|
|
4057ac54b8 | ||
|
|
6a0ce8914e | ||
|
|
03f4b1291e | ||
|
|
2e16457467 | ||
|
|
a1d7da4fec | ||
|
|
0522ae8155 | ||
|
|
55a246869e | ||
|
|
3ec3362ac4 | ||
|
|
3f01456480 | ||
|
|
9ea10763e4 | ||
|
|
e814852ca1 | ||
|
|
a102f9c6cf | ||
|
|
9ccf402ed1 | ||
|
|
61eb3e2a3b | ||
|
|
92002fa35e | ||
|
|
a424148018 | ||
|
|
d4d823df45 | ||
|
|
c45d27113e | ||
|
|
9fab332772 |
@@ -466,6 +466,17 @@ func buildServiceConfig(url *url.URL) ([]*config.ServiceConfig, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// stdio listener uses url.Host as the forwarding target.
|
||||
if listener == "stdio" && len(nodes) == 0 && url.Host != "" {
|
||||
nodes = append(nodes, &config.ForwardNodeConfig{
|
||||
Name: fmt.Sprintf("%starget-0", namePrefix),
|
||||
Addr: url.Host,
|
||||
})
|
||||
if handler == "auto" {
|
||||
handler = "tcp"
|
||||
}
|
||||
}
|
||||
|
||||
if len(nodes) > 0 {
|
||||
if len(services) == 1 {
|
||||
services[0].Forwarder = &config.ForwarderConfig{
|
||||
|
||||
@@ -34,11 +34,27 @@ import (
|
||||
"github.com/go-gost/x/metadata"
|
||||
mdutil "github.com/go-gost/x/metadata/util"
|
||||
xstats "github.com/go-gost/x/observer/stats"
|
||||
xrecorder "github.com/go-gost/x/recorder"
|
||||
"github.com/go-gost/x/registry"
|
||||
xservice "github.com/go-gost/x/service"
|
||||
"github.com/vishvananda/netns"
|
||||
)
|
||||
|
||||
// plaintextListeners are listener types that never terminate TLS on their
|
||||
// accepted connections, so any certFile/keyFile/caFile configured for them is
|
||||
// silently ignored. This catches the common footgun where a user writes e.g.
|
||||
// `tls+mws://...?certFile=...` expecting TLS: the "tls+" prefix is parsed as
|
||||
// the handler scheme (see x/config/cmd/cmd.go buildServiceConfig), not a TLS
|
||||
// wrapper, so the underlying mws listener stays plaintext. Extend this set when
|
||||
// new plaintext listeners are added.
|
||||
var plaintextListeners = map[string]bool{
|
||||
"tcp": true, "udp": true,
|
||||
"ws": true, "mws": true,
|
||||
"redirect": true, "tproxy": true,
|
||||
"rtcp": true, "rudp": true,
|
||||
"unix": true, "runix": true, "serial": true, "stdio": true,
|
||||
}
|
||||
|
||||
// ParseService constructs a fully-wired service.Service from a ServiceConfig.
|
||||
// It defaults the listener to "tcp" and the handler to "auto", resolves named
|
||||
// components from the registry (authers, admissions, bypasses, resolvers,
|
||||
@@ -88,6 +104,11 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
|
||||
tls_util.RejectUnknownSNIConfig(tlsConfig, tlsCfg.RejectUnknownSNI, tlsCfg.ServerNames)
|
||||
}
|
||||
|
||||
if (tlsCfg.CertFile != "" || tlsCfg.KeyFile != "" || tlsCfg.CAFile != "") && plaintextListeners[cfg.Listener.Type] {
|
||||
serviceLogger.Warnf("TLS certificate options are configured but the %q listener does not use TLS and will ignore them; the connection will be plaintext. Use a TLS-capable listener (e.g. mwss, wss, tls, quic, grpc, http2, http3) to enable TLS.",
|
||||
cfg.Listener.Type)
|
||||
}
|
||||
|
||||
authers := auth_parser.List(cfg.Listener.Auther, cfg.Listener.Authers...)
|
||||
if len(authers) == 0 {
|
||||
if auther := auth_parser.ParseAutherFromAuth(cfg.Listener.Auth); auther != nil {
|
||||
@@ -285,8 +306,15 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
|
||||
var recorders []recorder.RecorderObject
|
||||
for _, r := range cfg.Recorders {
|
||||
md := metadata.NewMetadata(r.Metadata)
|
||||
rec := registry.RecorderRegistry().Get(r.Name)
|
||||
if r.Metadata != nil {
|
||||
rec = &xrecorder.MetadataRecorder{
|
||||
Recorder: rec,
|
||||
Metadata: r.Metadata,
|
||||
}
|
||||
}
|
||||
recorders = append(recorders, recorder.RecorderObject{
|
||||
Recorder: registry.RecorderRegistry().Get(r.Name),
|
||||
Recorder: rec,
|
||||
Record: r.Record,
|
||||
Options: &recorder.Options{
|
||||
Direction: mdutil.GetBool(md, parsing.MDKeyRecorderDirection),
|
||||
@@ -295,6 +323,7 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
|
||||
HTTPBody: mdutil.GetBool(md, parsing.MDKeyRecorderHTTPBody),
|
||||
MaxBodySize: mdutil.GetInt(md, parsing.MDKeyRecorderHTTPMaxBodySize),
|
||||
},
|
||||
Metadata: r.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+98
-22
@@ -12,6 +12,8 @@ import (
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -24,6 +26,24 @@ var (
|
||||
defaultTLSConfig atomic.Value
|
||||
)
|
||||
|
||||
// testDefaultCertDir overrides the default certificate directory in tests.
|
||||
// It is set only from same-package test code and left empty in production.
|
||||
var testDefaultCertDir string
|
||||
|
||||
// defaultCertDir returns the directory used for persisting auto-generated
|
||||
// certificates. It uses testDefaultCertDir when set (for testing), otherwise
|
||||
// returns $HOME/.gost/.
|
||||
func defaultCertDir() string {
|
||||
if testDefaultCertDir != "" {
|
||||
return testDefaultCertDir
|
||||
}
|
||||
dir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return ".gost"
|
||||
}
|
||||
return filepath.Join(dir, ".gost")
|
||||
}
|
||||
|
||||
// DefaultTLSConfig returns the global default TLS configuration used as a
|
||||
// fallback when a listener or handler does not specify its own TLS settings.
|
||||
// The returned config is a shared instance; callers that need to mutate it
|
||||
@@ -40,44 +60,99 @@ func SetDefaultTLSConfig(cfg *tls.Config) {
|
||||
}
|
||||
|
||||
// BuildDefaultTLSConfig loads or generates the default TLS certificate and key
|
||||
// from the given config. If cfg is nil it attempts to load well-known files
|
||||
// ("cert.pem", "key.pem", "ca.pem"). On failure it generates a self-signed
|
||||
// ECDSA P-256 certificate valid for one year.
|
||||
// from the given config.
|
||||
//
|
||||
// When explicit certificate files are configured (CertFile, KeyFile, or CAFile
|
||||
// is non-empty) it loads them via tls_util.LoadDefaultConfig — the original
|
||||
// behaviour preserved for backward compatibility.
|
||||
//
|
||||
// When no certificate files are configured (all three empty), it tries sources
|
||||
// in this order:
|
||||
// 1. cert.pem / key.pem in the current working directory (backward compatible)
|
||||
// 2. auto-ca-cert.pem / auto-ca-key.pem under $HOME/.gost/ (persisted)
|
||||
// 3. If none found, generates a new ECDSA P-256 CA certificate, persists it
|
||||
// to $HOME/.gost/, and uses it.
|
||||
//
|
||||
// If cfg is nil an empty TLSConfig is used (equivalent to no explicit files),
|
||||
// so the CWD → persisted → generate path is taken.
|
||||
func BuildDefaultTLSConfig(cfg *config.TLSConfig) (*tls.Config, error) {
|
||||
log := logger.Default()
|
||||
|
||||
if cfg == nil {
|
||||
cfg = &config.TLSConfig{
|
||||
CertFile: "cert.pem",
|
||||
KeyFile: "key.pem",
|
||||
CAFile: "ca.pem",
|
||||
}
|
||||
cfg = &config.TLSConfig{}
|
||||
}
|
||||
|
||||
tlsConfig, err := tls_util.LoadDefaultConfig(cfg.CertFile, cfg.KeyFile, cfg.CAFile)
|
||||
if err != nil {
|
||||
// generate random self-signed certificate.
|
||||
cert, err := genCertificate(cfg.Validity, cfg.Organization, cfg.CommonName)
|
||||
if cfg.CertFile != "" || cfg.KeyFile != "" || cfg.CAFile != "" {
|
||||
tlsConfig, err := tls_util.LoadDefaultConfig(cfg.CertFile, cfg.KeyFile, cfg.CAFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig = &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
log.Debug("load global TLS certificate files failed, use random generated certificate")
|
||||
} else {
|
||||
log.Debug("load global TLS certificate files OK")
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
tlsConfig, err := loadOrGeneratePersistentTLSConfig(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
func genCertificate(validity time.Duration, org string, cn string) (cert tls.Certificate, err error) {
|
||||
rawCert, rawKey, err := generateKeyPair(validity, org, cn)
|
||||
if err != nil {
|
||||
return
|
||||
// loadOrGeneratePersistentTLSConfig tries loading an auto-generated CA
|
||||
// certificate from these locations (in order):
|
||||
// 1. cert.pem / key.pem in the current working directory (backward compatible)
|
||||
// 2. auto-ca-cert.pem / auto-ca-key.pem under defaultCertDir() (persisted)
|
||||
//
|
||||
// If none is found it generates a new one, persists it to defaultCertDir()
|
||||
// (best-effort), and returns it.
|
||||
func loadOrGeneratePersistentTLSConfig(cfg *config.TLSConfig) (*tls.Config, error) {
|
||||
log := logger.Default()
|
||||
|
||||
// 1. Try CWD cert.pem / key.pem first (backward compatible).
|
||||
cwdCert, cwdErr := tls.LoadX509KeyPair("cert.pem", "key.pem")
|
||||
if cwdErr == nil {
|
||||
log.Debug("loaded default certificate from current working directory (cert.pem / key.pem)")
|
||||
return &tls.Config{Certificates: []tls.Certificate{cwdCert}}, nil
|
||||
}
|
||||
return tls.X509KeyPair(rawCert, rawKey)
|
||||
|
||||
// 2. Try persisted certificate in defaultCertDir().
|
||||
dir := defaultCertDir()
|
||||
certFile := filepath.Join(dir, "auto-ca-cert.pem")
|
||||
keyFile := filepath.Join(dir, "auto-ca-key.pem")
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err == nil {
|
||||
log.Debugf("loaded persisted default certificate from %s", dir)
|
||||
return &tls.Config{Certificates: []tls.Certificate{cert}}, nil
|
||||
}
|
||||
|
||||
log.Debug("generating new default certificate (no persisted certificate found)")
|
||||
|
||||
// 3. Generate a new certificate.
|
||||
rawCert, rawKey, err := generateKeyPair(cfg.Validity, cfg.Organization, cfg.CommonName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Best-effort persist to disk. If it fails the in-memory certificate is
|
||||
// still usable.
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
log.Warnf("failed to create certificate directory %s: %v", dir, err)
|
||||
} else {
|
||||
if err := os.WriteFile(certFile, rawCert, 0644); err != nil {
|
||||
log.Warnf("failed to persist certificate: %v", err)
|
||||
} else if err := os.WriteFile(keyFile, rawKey, 0600); err != nil {
|
||||
log.Warnf("failed to persist private key: %v", err)
|
||||
} else {
|
||||
log.Debugf("persisted default certificate to %s", dir)
|
||||
}
|
||||
}
|
||||
|
||||
cert, err = tls.X509KeyPair(rawCert, rawKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tls.Config{Certificates: []tls.Certificate{cert}}, nil
|
||||
}
|
||||
|
||||
func generateKeyPair(validity time.Duration, org string, cn string) (rawCert, rawKey []byte, err error) {
|
||||
@@ -124,6 +199,7 @@ func generateKeyPair(validity time.Duration, org string, cn string) (rawCert, ra
|
||||
x509.ExtKeyUsageServerAuth,
|
||||
},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
if _, isRSA := priv.(*rsa.PrivateKey); isRSA {
|
||||
template.KeyUsage |= x509.KeyUsageKeyEncipherment
|
||||
|
||||
+181
-9
@@ -2,15 +2,58 @@ package parsing
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/x/config"
|
||||
xlogger "github.com/go-gost/x/logger"
|
||||
)
|
||||
|
||||
// writeTestCertKey writes a self-signed cert/key pair to the given file paths.
|
||||
// The cert is NOT persisted via the defaultCertDir mechanism — it simulates
|
||||
// user-provided cert.pem / key.pem in a working directory.
|
||||
func writeTestCertKey(t *testing.T, certFile, keyFile string) {
|
||||
t.Helper()
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatalf("generate key: %v", err)
|
||||
}
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{CommonName: "cwd-test.local"},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
t.Fatalf("create cert: %v", err)
|
||||
}
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
||||
})
|
||||
if err := os.WriteFile(certFile, certPEM, 0644); err != nil {
|
||||
t.Fatalf("write cert: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(keyFile, keyPEM, 0600); err != nil {
|
||||
t.Fatalf("write key: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
|
||||
m.Run()
|
||||
@@ -47,6 +90,10 @@ func TestDefaultTLSConfig_InitiallyNil(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuildDefaultTLSConfig_Nil(t *testing.T) {
|
||||
// Use a temp dir so the test is isolated and doesn't touch $HOME/.gost
|
||||
testDefaultCertDir = t.TempDir()
|
||||
t.Cleanup(func() { testDefaultCertDir = "" })
|
||||
|
||||
// BuildDefaultTLSConfig with nil should generate a self-signed cert
|
||||
tlsCfg, err := BuildDefaultTLSConfig(nil)
|
||||
if err != nil {
|
||||
@@ -58,9 +105,22 @@ func TestBuildDefaultTLSConfig_Nil(t *testing.T) {
|
||||
if len(tlsCfg.Certificates) == 0 {
|
||||
t.Fatal("expected at least one certificate")
|
||||
}
|
||||
|
||||
// Verify that the certificate and key files were persisted.
|
||||
certFile := filepath.Join(testDefaultCertDir, "auto-ca-cert.pem")
|
||||
keyFile := filepath.Join(testDefaultCertDir, "auto-ca-key.pem")
|
||||
if _, err := os.Stat(certFile); os.IsNotExist(err) {
|
||||
t.Fatal("cert file was not persisted")
|
||||
}
|
||||
if _, err := os.Stat(keyFile); os.IsNotExist(err) {
|
||||
t.Fatal("key file was not persisted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDefaultTLSConfig_WithOptions(t *testing.T) {
|
||||
testDefaultCertDir = t.TempDir()
|
||||
t.Cleanup(func() { testDefaultCertDir = "" })
|
||||
|
||||
cfg := &config.TLSConfig{
|
||||
Validity: 0, // uses default
|
||||
Organization: "TestOrg",
|
||||
@@ -78,26 +138,138 @@ func TestBuildDefaultTLSConfig_WithOptions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDefaultTLSConfig_TwoCallsDifferentCerts(t *testing.T) {
|
||||
func TestBuildDefaultTLSConfig_TwoCallsSameCerts(t *testing.T) {
|
||||
testDefaultCertDir = t.TempDir()
|
||||
t.Cleanup(func() { testDefaultCertDir = "" })
|
||||
|
||||
cfg1, err := BuildDefaultTLSConfig(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
t.Fatalf("unexpected error on first call: %v", err)
|
||||
}
|
||||
cfg2, err := BuildDefaultTLSConfig(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
t.Fatalf("unexpected error on second call: %v", err)
|
||||
}
|
||||
if len(cfg1.Certificates) == 0 || len(cfg2.Certificates) == 0 {
|
||||
t.Fatal("expected certificates in both configs")
|
||||
}
|
||||
|
||||
// Each call generates a random cert; they should have different raw bytes.
|
||||
if len(cfg1.Certificates[0].Certificate) == 0 || len(cfg2.Certificates[0].Certificate) == 0 {
|
||||
t.Fatal("expected non-empty certificate raw bytes")
|
||||
}
|
||||
// Both calls should return the SAME persisted certificate.
|
||||
c1 := cfg1.Certificates[0].Certificate[0]
|
||||
c2 := cfg2.Certificates[0].Certificate[0]
|
||||
if bytes.Equal(c1, c2) {
|
||||
t.Fatal("expected different certificates from two calls")
|
||||
if !bytes.Equal(c1, c2) {
|
||||
t.Fatal("expected same certificate from two calls (persisted)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDefaultTLSConfig_PersistsAndReloads(t *testing.T) {
|
||||
testDefaultCertDir = t.TempDir()
|
||||
defer func() { testDefaultCertDir = "" }()
|
||||
|
||||
// First call generates and persists.
|
||||
cfg1, err := BuildDefaultTLSConfig(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error on first call: %v", err)
|
||||
}
|
||||
|
||||
// Verify files exist on disk.
|
||||
certFile := filepath.Join(testDefaultCertDir, "auto-ca-cert.pem")
|
||||
keyFile := filepath.Join(testDefaultCertDir, "auto-ca-key.pem")
|
||||
if _, err := os.Stat(certFile); os.IsNotExist(err) {
|
||||
t.Fatal("cert file was not persisted after first call")
|
||||
}
|
||||
if _, err := os.Stat(keyFile); os.IsNotExist(err) {
|
||||
t.Fatal("key file was not persisted after first call")
|
||||
}
|
||||
|
||||
// Second call loads the persisted cert.
|
||||
cfg2, err := BuildDefaultTLSConfig(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error on second call: %v", err)
|
||||
}
|
||||
|
||||
c1 := cfg1.Certificates[0].Certificate[0]
|
||||
c2 := cfg2.Certificates[0].Certificate[0]
|
||||
if !bytes.Equal(c1, c2) {
|
||||
t.Fatal("expected same certificate on reload (persisted)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDefaultTLSConfig_FallsBackWhenWriteBlocked(t *testing.T) {
|
||||
// Point to a directory that can't be created (e.g. a file path).
|
||||
// We use a path under /proc on Linux which is a read-only filesystem.
|
||||
testDefaultCertDir = "/proc/doesnotexist/certdir"
|
||||
defer func() { testDefaultCertDir = "" }()
|
||||
|
||||
tlsCfg, err := BuildDefaultTLSConfig(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error (should fall back to in-memory): %v", err)
|
||||
}
|
||||
if tlsCfg == nil {
|
||||
t.Fatal("expected non-nil config even when write fails")
|
||||
}
|
||||
if len(tlsCfg.Certificates) == 0 {
|
||||
t.Fatal("expected at least one certificate from fallback")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildDefaultTLSConfig_CWDTakesPriority verifies that when cert.pem and
|
||||
// key.pem exist in the current working directory, they take precedence over
|
||||
// both the persisted certificate and auto-generation.
|
||||
func TestBuildDefaultTLSConfig_CWDTakesPriority(t *testing.T) {
|
||||
// Create a temp dir with cert.pem / key.pem.
|
||||
cwd := t.TempDir()
|
||||
writeTestCertKey(t, filepath.Join(cwd, "cert.pem"), filepath.Join(cwd, "key.pem"))
|
||||
|
||||
// Also set up a persisted cert directory.
|
||||
testDefaultCertDir = t.TempDir()
|
||||
defer func() { testDefaultCertDir = "" }()
|
||||
|
||||
// Pre-populate persisted cert so we can prove CWD wins.
|
||||
_, err := BuildDefaultTLSConfig(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("pre-populate persisted cert: %v", err)
|
||||
}
|
||||
|
||||
// Change to the CWD that has cert.pem / key.pem.
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("getwd: %v", err)
|
||||
}
|
||||
if err := os.Chdir(cwd); err != nil {
|
||||
t.Fatalf("chdir: %v", err)
|
||||
}
|
||||
defer os.Chdir(origDir)
|
||||
|
||||
// BuildDefaultTLSConfig should load CWD cert, not the persisted one.
|
||||
tlsCfg, err := BuildDefaultTLSConfig(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(tlsCfg.Certificates) == 0 {
|
||||
t.Fatal("expected at least one certificate")
|
||||
}
|
||||
|
||||
// Load CWD cert directly and compare.
|
||||
cwdCert, err := tls.LoadX509KeyPair("cert.pem", "key.pem")
|
||||
if err != nil {
|
||||
t.Fatalf("load CWD cert: %v", err)
|
||||
}
|
||||
got := tlsCfg.Certificates[0].Certificate[0]
|
||||
want := cwdCert.Certificate[0]
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatal("CWD certificate was not used (persisted or auto-generated cert took priority)")
|
||||
}
|
||||
|
||||
// Load persisted cert to prove it's different from CWD cert.
|
||||
persistedCert, err := tls.LoadX509KeyPair(
|
||||
filepath.Join(testDefaultCertDir, "auto-ca-cert.pem"),
|
||||
filepath.Join(testDefaultCertDir, "auto-ca-key.pem"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("load persisted cert: %v", err)
|
||||
}
|
||||
if bytes.Equal(got, persistedCert.Certificate[0]) {
|
||||
t.Fatal("CWD cert should differ from persisted cert; test setup issue")
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -3,6 +3,7 @@ package quic
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
)
|
||||
@@ -12,7 +13,9 @@ type quicSession struct {
|
||||
}
|
||||
|
||||
func (session *quicSession) GetConn() (*quicConn, error) {
|
||||
stream, err := session.session.OpenStreamSync(context.Background())
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
stream, err := session.session.OpenStreamSync(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@ func (d *icmpDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO
|
||||
}
|
||||
|
||||
d.sessionMutex.Lock()
|
||||
defer d.sessionMutex.Unlock()
|
||||
|
||||
session, ok := d.sessions[addr]
|
||||
if session != nil && session.IsClosed() {
|
||||
@@ -95,6 +94,7 @@ func (d *icmpDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO
|
||||
pc, err = icmp_pkg.ListenPacket("ip4:icmp", "")
|
||||
}
|
||||
if err != nil {
|
||||
d.sessionMutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -109,16 +109,22 @@ func (d *icmpDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO
|
||||
if err != nil {
|
||||
d.logger.Error(err)
|
||||
pc.Close()
|
||||
d.sessionMutex.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.sessions[addr] = session
|
||||
}
|
||||
d.sessionMutex.Unlock()
|
||||
|
||||
conn, err = session.GetConn()
|
||||
if err != nil {
|
||||
d.sessionMutex.Lock()
|
||||
if d.sessions[addr] == session {
|
||||
delete(d.sessions, addr)
|
||||
}
|
||||
session.Close()
|
||||
delete(d.sessions, addr)
|
||||
d.sessionMutex.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
+11
-4
@@ -14,14 +14,21 @@ type metadata struct {
|
||||
}
|
||||
|
||||
func (d *icmpDialer) parseMetadata(md mdata.Metadata) (err error) {
|
||||
if mdutil.GetBool(md, "keepalive") {
|
||||
d.md.keepAlivePeriod = mdutil.GetDuration(md, "ttl")
|
||||
const (
|
||||
keepAlive = "keepAlive"
|
||||
keepAlivePeriod = "ttl"
|
||||
handshakeTimeout = "handshakeTimeout"
|
||||
maxIdleTimeout = "maxIdleTimeout"
|
||||
)
|
||||
|
||||
if md == nil || !md.IsExists(keepAlive) || mdutil.GetBool(md, keepAlive) {
|
||||
d.md.keepAlivePeriod = mdutil.GetDuration(md, keepAlivePeriod)
|
||||
if d.md.keepAlivePeriod <= 0 {
|
||||
d.md.keepAlivePeriod = 10 * time.Second
|
||||
}
|
||||
}
|
||||
d.md.handshakeTimeout = mdutil.GetDuration(md, "handshakeTimeout")
|
||||
d.md.maxIdleTimeout = mdutil.GetDuration(md, "maxIdleTimeout")
|
||||
d.md.handshakeTimeout = mdutil.GetDuration(md, handshakeTimeout)
|
||||
d.md.maxIdleTimeout = mdutil.GetDuration(md, maxIdleTimeout)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
+4
-1
@@ -3,6 +3,7 @@ package quic
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
)
|
||||
@@ -12,7 +13,9 @@ type quicSession struct {
|
||||
}
|
||||
|
||||
func (session *quicSession) GetConn() (*quicConn, error) {
|
||||
stream, err := session.session.OpenStreamSync(context.Background())
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
stream, err := session.session.OpenStreamSync(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -58,7 +58,6 @@ func (d *quicDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO
|
||||
}
|
||||
|
||||
d.sessionMutex.Lock()
|
||||
defer d.sessionMutex.Unlock()
|
||||
|
||||
session, ok := d.sessions[addr]
|
||||
if session != nil && session.IsClosed() {
|
||||
@@ -73,11 +72,13 @@ func (d *quicDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO
|
||||
|
||||
c, err := options.Dialer.Dial(ctx, "udp", "")
|
||||
if err != nil {
|
||||
d.sessionMutex.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
pc, ok := c.(net.PacketConn)
|
||||
if !ok {
|
||||
c.Close()
|
||||
d.sessionMutex.Unlock()
|
||||
return nil, errors.New("quic: wrong connection type")
|
||||
}
|
||||
|
||||
@@ -89,16 +90,22 @@ func (d *quicDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO
|
||||
if err != nil {
|
||||
d.logger.Error(err)
|
||||
pc.Close()
|
||||
d.sessionMutex.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.sessions[addr] = session
|
||||
}
|
||||
d.sessionMutex.Unlock()
|
||||
|
||||
conn, err = session.GetConn()
|
||||
if err != nil {
|
||||
d.sessionMutex.Lock()
|
||||
if d.sessions[addr] == session {
|
||||
delete(d.sessions, addr)
|
||||
}
|
||||
session.Close()
|
||||
delete(d.sessions, addr)
|
||||
d.sessionMutex.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,18 @@ func (d *sshdDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO
|
||||
delete(d.sessions, addr) // session is dead
|
||||
ok = false
|
||||
}
|
||||
if ok {
|
||||
// Health check: verify the cached session is still alive before
|
||||
// returning it. A dead-but-not-yet-detected session (e.g. idle
|
||||
// connection silently dropped by NAT) would cause "ssh: unexpected
|
||||
// packet in response to channel open" later in the connector.
|
||||
if err := session.Ping(d.md.keepaliveTimeout); err != nil {
|
||||
d.options.Logger.Debugf("sshd session ping failed: %v, recreating", err)
|
||||
session.Close()
|
||||
delete(d.sessions, addr)
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
var options dialer.DialOptions
|
||||
for _, opt := range opts {
|
||||
|
||||
@@ -7,12 +7,12 @@ require (
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
|
||||
github.com/gin-contrib/cors v1.7.2
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/go-gost/core v0.4.1
|
||||
github.com/go-gost/core v0.4.2
|
||||
github.com/go-gost/go-shadowsocks2 v0.1.3
|
||||
github.com/go-gost/gosocks4 v0.1.0
|
||||
github.com/go-gost/gosocks5 v0.5.0
|
||||
github.com/go-gost/plugin v0.3.0
|
||||
github.com/go-gost/relay v0.6.0
|
||||
github.com/go-gost/relay v0.6.1
|
||||
github.com/go-gost/tls-dissector v0.2.0
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/gobwas/glob v0.2.3
|
||||
@@ -60,6 +60,7 @@ require (
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -51,8 +51,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-gost/core v0.4.1 h1:/MZYv0tSkw9yoK2nl0GLl3GjT1/3qKl5u3E/DADyeZE=
|
||||
github.com/go-gost/core v0.4.1/go.mod h1:WGI43jOka7FAsSAwi/fSMaqxdR+E339ycb4NBGlFr6A=
|
||||
github.com/go-gost/core v0.4.2 h1:NBCkdWItIiwt15cA2mkRza/u6ukxQVSW74bc33o70zE=
|
||||
github.com/go-gost/core v0.4.2/go.mod h1:WGI43jOka7FAsSAwi/fSMaqxdR+E339ycb4NBGlFr6A=
|
||||
github.com/go-gost/go-shadowsocks2 v0.1.3 h1:6CUZLp+mTWXnKP2aK8/Z9ZP+ERMX9gSbywmPu4kGX/A=
|
||||
github.com/go-gost/go-shadowsocks2 v0.1.3/go.mod h1:866zFNNI3He6Wef1M/IvAjTal74WhcfKfBgRpTlkKys=
|
||||
github.com/go-gost/gosocks4 v0.1.0 h1:eAzev6qw4fzkFQKC9uCHLVNnnPdHyqCggbnfNN80Pmk=
|
||||
@@ -61,8 +61,8 @@ github.com/go-gost/gosocks5 v0.5.0 h1:YE37l1MJwde8diIQdynStqogMotG5enoTdborhA5yi
|
||||
github.com/go-gost/gosocks5 v0.5.0/go.mod h1:1G6I7HP7VFVxveGkoK8mnprnJqSqJjdcASKsdUn4Pp4=
|
||||
github.com/go-gost/plugin v0.3.0 h1:pmll8nNd9PX92BWMB5+b2y2SEkBAWJLxD2ANfX+WHuw=
|
||||
github.com/go-gost/plugin v0.3.0/go.mod h1:oN23l+yGDCIP9G3KnDl/I/0zVGOobZUDCB2Z5yYYXts=
|
||||
github.com/go-gost/relay v0.6.0 h1:o8r0B3eTw8Rwwent+5x5Cw1g4AyNUl4hNqiyDJXkFvA=
|
||||
github.com/go-gost/relay v0.6.0/go.mod h1:Dku0f5sfjOClrZFiDmQUrYYJ4uof7rnkCUBfsl0PSAI=
|
||||
github.com/go-gost/relay v0.6.1 h1:7SqnHFbY8x/DzvjpK03a5zcVH9+TbJAcnW/RT6s1ecc=
|
||||
github.com/go-gost/relay v0.6.1/go.mod h1:Dku0f5sfjOClrZFiDmQUrYYJ4uof7rnkCUBfsl0PSAI=
|
||||
github.com/go-gost/tls-dissector v0.2.0 h1:9tE6WOzzpurATTBWn60DU4R8gibpGNY8/qVcc1SicVg=
|
||||
github.com/go-gost/tls-dissector v0.2.0/go.mod h1:/9QfdewqmHdaE362Hv5nDaSWLx3pCmtD870d6GaquXs=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
@@ -206,6 +206,8 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28=
|
||||
github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
|
||||
+45
-1
@@ -2,6 +2,7 @@ package auto
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
@@ -12,7 +13,10 @@ import (
|
||||
"github.com/go-gost/gosocks4"
|
||||
"github.com/go-gost/gosocks5"
|
||||
ctxvalue "github.com/go-gost/x/ctx"
|
||||
ictx "github.com/go-gost/x/internal/ctx"
|
||||
xnet "github.com/go-gost/x/internal/net"
|
||||
"github.com/go-gost/x/internal/util/sniffing"
|
||||
xrecorder "github.com/go-gost/x/recorder"
|
||||
"github.com/go-gost/x/registry"
|
||||
)
|
||||
|
||||
@@ -94,6 +98,16 @@ func (h *autoHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler
|
||||
}
|
||||
|
||||
br := bufio.NewReader(conn)
|
||||
proto, _ := sniffing.Sniff(ctx, br)
|
||||
|
||||
conn = xnet.NewReadWriteConn(br, conn, conn)
|
||||
|
||||
if proto == sniffing.ProtoTLS {
|
||||
return h.handleTLS(ctx, conn, log)
|
||||
}
|
||||
|
||||
// Fall back to 1-byte peek for SOCKS4 (0x04) vs SOCKS5 (0x05) vs HTTP.
|
||||
// Sniff() already read 5 bytes, but the buffered reader exposes them.
|
||||
b, err := br.Peek(1)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
@@ -101,7 +115,6 @@ func (h *autoHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler
|
||||
return err
|
||||
}
|
||||
|
||||
conn = xnet.NewReadWriteConn(br, conn, conn)
|
||||
switch b[0] {
|
||||
case gosocks4.Ver4: // socks4
|
||||
if h.socks4Handler != nil {
|
||||
@@ -118,3 +131,34 @@ func (h *autoHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleTLS forwards a TLS connection that arrived at the auto handler.
|
||||
// It parses the ClientHello for SNI, dials upstream through the router,
|
||||
// forwards the ClientHello, records ServerHello metadata, and pipes data
|
||||
// bidirectionally.
|
||||
func (h *autoHandler) handleTLS(ctx context.Context, conn net.Conn, log logger.Logger) error {
|
||||
ro := &xrecorder.HandlerRecorderObject{
|
||||
Network: "tcp",
|
||||
Service: h.options.Service,
|
||||
RemoteAddr: conn.RemoteAddr().String(),
|
||||
LocalAddr: conn.LocalAddr().String(),
|
||||
SID: ctxvalue.SidFromContext(ctx).String(),
|
||||
Time: time.Now(),
|
||||
}
|
||||
sniffer := &sniffing.Sniffer{}
|
||||
dial := func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
var buf bytes.Buffer
|
||||
cc, err := h.options.Router.Dial(ictx.ContextWithBuffer(ctx, &buf), "tcp", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ro.Route = buf.String()
|
||||
return cc, nil
|
||||
}
|
||||
return sniffer.HandleTLS(ctx, "tcp", conn,
|
||||
sniffing.WithDial(dial),
|
||||
sniffing.WithBypass(h.options.Bypass),
|
||||
sniffing.WithRecorderObject(ro),
|
||||
sniffing.WithLog(log),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ import (
|
||||
|
||||
"github.com/go-gost/core/bypass"
|
||||
"github.com/go-gost/core/limiter"
|
||||
"github.com/go-gost/core/recorder"
|
||||
stats "github.com/go-gost/core/observer/stats"
|
||||
"github.com/go-gost/core/logger"
|
||||
stats "github.com/go-gost/core/observer/stats"
|
||||
"github.com/go-gost/core/recorder"
|
||||
xctx "github.com/go-gost/x/ctx"
|
||||
ictx "github.com/go-gost/x/internal/ctx"
|
||||
xnet "github.com/go-gost/x/internal/net"
|
||||
@@ -132,6 +132,7 @@ func (h *httpHandler) sniffAndHandle(ctx context.Context, conn net.Conn, cc net.
|
||||
sniffing.WithService(h.options.Service),
|
||||
sniffing.WithDial(dial),
|
||||
sniffing.WithDialTLS(dialTLS),
|
||||
sniffing.WithBypass(h.options.Bypass),
|
||||
sniffing.WithRecorderObject(ro),
|
||||
sniffing.WithLog(log),
|
||||
)
|
||||
@@ -140,6 +141,7 @@ func (h *httpHandler) sniffAndHandle(ctx context.Context, conn net.Conn, cc net.
|
||||
sniffing.WithService(h.options.Service),
|
||||
sniffing.WithDial(dial),
|
||||
sniffing.WithDialTLS(dialTLS),
|
||||
sniffing.WithBypass(h.options.Bypass),
|
||||
sniffing.WithRecorderObject(ro),
|
||||
sniffing.WithLog(log),
|
||||
)
|
||||
@@ -233,7 +235,7 @@ type SnifferBuilder struct {
|
||||
// ReadTimeout is the timeout for reading upstream HTTP response headers
|
||||
// and TLS ServerHello during sniffing. Passed through to sniffing.Sniffer.
|
||||
// See sniffing.Sniffer.ReadTimeout for details.
|
||||
ReadTimeout time.Duration
|
||||
ReadTimeout time.Duration
|
||||
}
|
||||
|
||||
// Build creates a new sniffing.Sniffer from the builder's configuration.
|
||||
|
||||
+17
-14
@@ -445,20 +445,9 @@ func (h *httpHandler) handleRequest(ctx context.Context, conn net.Conn, req *htt
|
||||
ro.HTTP.Response.Header = resp.Header
|
||||
}()
|
||||
|
||||
// HTTP/2 connection preface "PRI * HTTP/2.0" is rejected, as are
|
||||
// non-CONNECT requests without an http:// scheme.
|
||||
if req.Method == "PRI" ||
|
||||
(req.Method != http.MethodConnect && req.URL.Scheme != "http") {
|
||||
resp.StatusCode = http.StatusBadRequest
|
||||
|
||||
if log.IsLevelEnabled(logger.TraceLevel) {
|
||||
dump, _ := httputil.DumpResponse(resp, false)
|
||||
log.Trace(string(dump))
|
||||
}
|
||||
|
||||
return resp.Write(conn)
|
||||
}
|
||||
|
||||
// Authenticate before request validation so that probe resistance
|
||||
// can intercept non-proxy-form requests (e.g., browser/scanner probes
|
||||
// that send "GET / HTTP/1.1" instead of proxy-form URLs).
|
||||
result := h.auth.Authenticate(ctx, req)
|
||||
if !result.OK {
|
||||
if result.PipeTo != "" {
|
||||
@@ -477,6 +466,20 @@ func (h *httpHandler) handleRequest(ctx context.Context, conn net.Conn, req *htt
|
||||
return errors.New("authentication failed")
|
||||
}
|
||||
|
||||
// HTTP/2 connection preface "PRI * HTTP/2.0" is rejected, as are
|
||||
// non-CONNECT requests without an http:// scheme.
|
||||
if req.Method == "PRI" ||
|
||||
(req.Method != http.MethodConnect && req.URL.Scheme != "http") {
|
||||
resp.StatusCode = http.StatusBadRequest
|
||||
|
||||
if log.IsLevelEnabled(logger.TraceLevel) {
|
||||
dump, _ := httputil.DumpResponse(resp, false)
|
||||
log.Trace(string(dump))
|
||||
}
|
||||
|
||||
return resp.Write(conn)
|
||||
}
|
||||
|
||||
log = log.WithFields(map[string]any{"clientID": result.ClientID})
|
||||
ro.ClientID = result.ClientID
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ func (h *metricsHandler) Init(md md.Metadata) (err error) {
|
||||
|
||||
xmetrics.Enable(true)
|
||||
|
||||
h.handler = promhttp.Handler()
|
||||
h.handler = promhttp.HandlerFor(xmetrics.Registry(), promhttp.HandlerOpts{})
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle(h.md.path, http.HandlerFunc(h.handleFunc))
|
||||
|
||||
@@ -237,6 +237,7 @@ func (h *relayHandler) handleConnect(ctx context.Context, conn net.Conn, network
|
||||
sniffing.WithService(h.options.Service),
|
||||
sniffing.WithDial(dial),
|
||||
sniffing.WithDialTLS(dialTLS),
|
||||
sniffing.WithBypass(h.options.Bypass),
|
||||
sniffing.WithRecorderObject(ro),
|
||||
sniffing.WithLog(log),
|
||||
)
|
||||
@@ -245,6 +246,7 @@ func (h *relayHandler) handleConnect(ctx context.Context, conn net.Conn, network
|
||||
sniffing.WithService(h.options.Service),
|
||||
sniffing.WithDial(dial),
|
||||
sniffing.WithDialTLS(dialTLS),
|
||||
sniffing.WithBypass(h.options.Bypass),
|
||||
sniffing.WithRecorderObject(ro),
|
||||
sniffing.WithLog(log),
|
||||
)
|
||||
@@ -306,4 +308,4 @@ func (a *serialAddr) Network() string {
|
||||
|
||||
func (a *serialAddr) String() string {
|
||||
return a.port
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +310,7 @@ func (h *socks4Handler) handleConnect(ctx context.Context, conn net.Conn, req *g
|
||||
sniffing.WithService(h.options.Service),
|
||||
sniffing.WithDial(dial),
|
||||
sniffing.WithDialTLS(dialTLS),
|
||||
sniffing.WithBypass(h.options.Bypass),
|
||||
sniffing.WithRecorderObject(ro),
|
||||
sniffing.WithLog(log),
|
||||
)
|
||||
@@ -318,6 +319,7 @@ func (h *socks4Handler) handleConnect(ctx context.Context, conn net.Conn, req *g
|
||||
sniffing.WithService(h.options.Service),
|
||||
sniffing.WithDial(dial),
|
||||
sniffing.WithDialTLS(dialTLS),
|
||||
sniffing.WithBypass(h.options.Bypass),
|
||||
sniffing.WithRecorderObject(ro),
|
||||
sniffing.WithLog(log),
|
||||
)
|
||||
|
||||
@@ -128,6 +128,7 @@ func (h *socks5Handler) handleConnect(ctx context.Context, conn net.Conn, networ
|
||||
sniffing.WithService(h.options.Service),
|
||||
sniffing.WithDial(dial),
|
||||
sniffing.WithDialTLS(dialTLS),
|
||||
sniffing.WithBypass(h.options.Bypass),
|
||||
sniffing.WithRecorderObject(ro),
|
||||
sniffing.WithLog(log),
|
||||
)
|
||||
@@ -136,6 +137,7 @@ func (h *socks5Handler) handleConnect(ctx context.Context, conn net.Conn, networ
|
||||
sniffing.WithService(h.options.Service),
|
||||
sniffing.WithDial(dial),
|
||||
sniffing.WithDialTLS(dialTLS),
|
||||
sniffing.WithBypass(h.options.Bypass),
|
||||
sniffing.WithRecorderObject(ro),
|
||||
sniffing.WithLog(log),
|
||||
)
|
||||
|
||||
@@ -100,6 +100,26 @@ func (h *socks5Handler) Init(md md.Metadata) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// networkAddr derives the network string from the SOCKS5 address type.
|
||||
// For IPv4 addresses it returns "tcp4"/"udp4" to prevent dual-stack
|
||||
// (IPv4-mapped-IPv6) socket creation on Linux, which would cause the
|
||||
// listener to bind on [::]:port instead of 0.0.0.0:port.
|
||||
// For IPv6 addresses it returns "tcp6"/"udp6".
|
||||
// For domain addresses it returns the unmodified network.
|
||||
func networkAddr(network string, addr *gosocks5.Addr) string {
|
||||
if addr == nil {
|
||||
return network
|
||||
}
|
||||
switch addr.Type {
|
||||
case gosocks5.AddrIPv4:
|
||||
return network + "4"
|
||||
case gosocks5.AddrIPv6:
|
||||
return network + "6"
|
||||
default:
|
||||
return network
|
||||
}
|
||||
}
|
||||
|
||||
func (h *socks5Handler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) (err error) {
|
||||
defer conn.Close()
|
||||
|
||||
@@ -179,15 +199,15 @@ func (h *socks5Handler) Handle(ctx context.Context, conn net.Conn, opts ...handl
|
||||
case gosocks5.CmdConnect:
|
||||
return h.handleConnect(ctx, conn, "tcp", address, ro, log)
|
||||
case gosocks5.CmdBind:
|
||||
return h.handleBind(ctx, conn, "tcp", address, ro, log)
|
||||
return h.handleBind(ctx, conn, networkAddr("tcp", req.Addr), address, ro, log)
|
||||
case socks.CmdMuxBind:
|
||||
return h.handleMuxBind(ctx, conn, "tcp", address, ro, log)
|
||||
return h.handleMuxBind(ctx, conn, networkAddr("tcp", req.Addr), address, ro, log)
|
||||
case gosocks5.CmdUdp:
|
||||
ro.Network = "udp"
|
||||
return h.handleUDP(ctx, conn, "udp", ro, log)
|
||||
case socks.CmdUDPTun:
|
||||
ro.Network = "udp"
|
||||
return h.handleUDPTun(ctx, conn, "udp", address, ro, log)
|
||||
return h.handleUDPTun(ctx, conn, networkAddr("udp", req.Addr), address, ro, log)
|
||||
default:
|
||||
err = ErrUnknownCmd
|
||||
log.Error(err)
|
||||
|
||||
@@ -26,6 +26,7 @@ type metadata struct {
|
||||
enableBind bool
|
||||
enableUDP bool
|
||||
udpBufferSize int
|
||||
udpResolveDomain bool
|
||||
compatibilityMode bool
|
||||
hash string
|
||||
muxCfg *mux.Config
|
||||
@@ -58,6 +59,7 @@ func (h *socks5Handler) parseMetadata(md mdata.Metadata) (err error) {
|
||||
h.md.enableBind = mdutil.GetBool(md, "bind")
|
||||
h.md.enableUDP = mdutil.GetBool(md, "udp")
|
||||
h.md.udpBufferSize = mdutil.GetInt(md, "udp.bufferSize", "udpBufferSize")
|
||||
h.md.udpResolveDomain = mdutil.GetBool(md, "udp.resolveDomain", "udpResolveDomain")
|
||||
|
||||
h.md.compatibilityMode = mdutil.GetBool(md, "comp")
|
||||
h.md.hash = mdutil.GetString(md, "hash")
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package v5
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-gost/gosocks5"
|
||||
)
|
||||
|
||||
func TestNetworkAddr(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
network string
|
||||
addr *gosocks5.Addr
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "nil addr returns unmodified",
|
||||
network: "tcp",
|
||||
addr: nil,
|
||||
want: "tcp",
|
||||
},
|
||||
{
|
||||
name: "IPv4 tcp returns tcp4",
|
||||
network: "tcp",
|
||||
addr: &gosocks5.Addr{Type: gosocks5.AddrIPv4},
|
||||
want: "tcp4",
|
||||
},
|
||||
{
|
||||
name: "IPv6 tcp returns tcp6",
|
||||
network: "tcp",
|
||||
addr: &gosocks5.Addr{Type: gosocks5.AddrIPv6},
|
||||
want: "tcp6",
|
||||
},
|
||||
{
|
||||
name: "IPv4 udp returns udp4",
|
||||
network: "udp",
|
||||
addr: &gosocks5.Addr{Type: gosocks5.AddrIPv4},
|
||||
want: "udp4",
|
||||
},
|
||||
{
|
||||
name: "IPv6 udp returns udp6",
|
||||
network: "udp",
|
||||
addr: &gosocks5.Addr{Type: gosocks5.AddrIPv6},
|
||||
want: "udp6",
|
||||
},
|
||||
{
|
||||
name: "domain addr returns unmodified",
|
||||
network: "tcp",
|
||||
addr: &gosocks5.Addr{Type: gosocks5.AddrDomain},
|
||||
want: "tcp",
|
||||
},
|
||||
{
|
||||
name: "unknown addr type returns unmodified",
|
||||
network: "udp",
|
||||
addr: &gosocks5.Addr{Type: 99},
|
||||
want: "udp",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := networkAddr(tt.network, tt.addr)
|
||||
if got != tt.want {
|
||||
t.Errorf("networkAddr(%q, %v) = %q, want %q", tt.network, tt.addr, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+75
-2
@@ -6,11 +6,14 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/hosts"
|
||||
"github.com/go-gost/core/limiter"
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/observer/stats"
|
||||
"github.com/go-gost/core/resolver"
|
||||
"github.com/go-gost/gosocks5"
|
||||
ctxvalue "github.com/go-gost/x/ctx"
|
||||
ictx "github.com/go-gost/x/internal/ctx"
|
||||
@@ -129,12 +132,23 @@ func (h *socks5Handler) handleUDP(ctx context.Context, conn net.Conn, network st
|
||||
}
|
||||
}
|
||||
|
||||
r := udp.NewRelay(socks.UDPConn(
|
||||
pc1 := socks.UDPConn(
|
||||
&filteredPacketConn{
|
||||
PacketConn: cc,
|
||||
filterIP: conn.RemoteAddr().(*net.TCPAddr).IP,
|
||||
},
|
||||
h.md.udpBufferSize), pc).
|
||||
h.md.udpBufferSize)
|
||||
|
||||
if h.md.udpResolveDomain {
|
||||
pc1 = &domainResolvePacketConn{
|
||||
PacketConn: pc1,
|
||||
resolver: h.options.Router.Options().Resolver,
|
||||
hostMapper: h.options.Router.Options().HostMapper,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
r := udp.NewRelay(pc1, pc).
|
||||
WithService(h.options.Service).
|
||||
WithBypass(h.options.Bypass).
|
||||
WithBufferSize(h.md.udpBufferSize).
|
||||
@@ -170,3 +184,62 @@ func (f *filteredPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// domainResolvePacketConn wraps a net.PacketConn and resolves domain
|
||||
// addresses to IPs in WriteTo calls. This guarantees that SOCKS5 UDP
|
||||
// response datagrams never carry ATYP=Domain (0x03), which some clients
|
||||
// (e.g., tun2proxy, Surge) cannot parse.
|
||||
type domainResolvePacketConn struct {
|
||||
net.PacketConn
|
||||
resolver resolver.Resolver
|
||||
hostMapper hosts.HostMapper
|
||||
log logger.Logger
|
||||
}
|
||||
|
||||
func (c *domainResolvePacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
||||
host, portStr, err := net.SplitHostPort(addr.String())
|
||||
if err != nil {
|
||||
return c.PacketConn.WriteTo(b, addr)
|
||||
}
|
||||
if net.ParseIP(host) != nil {
|
||||
// Already an IP address — pass through unchanged.
|
||||
return c.PacketConn.WriteTo(b, addr)
|
||||
}
|
||||
|
||||
// Resolve the domain using the router's infrastructure
|
||||
// (host mapper → resolver → system fallback) so the SOCKS5 encoder
|
||||
// will use AddrIPv4 or AddrIPv6 instead of AddrDomain.
|
||||
var ips []net.IP
|
||||
if c.hostMapper != nil {
|
||||
ips, _ = c.hostMapper.Lookup(context.Background(), "ip", host)
|
||||
}
|
||||
if len(ips) == 0 && c.resolver != nil {
|
||||
ips, err = c.resolver.Resolve(context.Background(), "ip", host)
|
||||
if err != nil && c.log != nil {
|
||||
c.log.Warnf("socks5 udp: resolve %s: %v", host, err)
|
||||
}
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
ips, err = net.LookupIP(host)
|
||||
}
|
||||
if err != nil || len(ips) == 0 {
|
||||
// Cannot resolve the domain — drop the datagram rather than
|
||||
// forwarding an ATYP=Domain response the client cannot parse.
|
||||
if c.log != nil {
|
||||
c.log.Warnf("socks5 udp: dropping datagram to %s (DNS resolution failed)", host)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Pick the first IPv4 address from the result set, falling back to
|
||||
// the first entry when no IPv4 is available.
|
||||
ip := ips[0]
|
||||
for _, candidate := range ips {
|
||||
if candidate.To4() != nil {
|
||||
ip = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
return c.PacketConn.WriteTo(b, &net.UDPAddr{IP: ip, Port: port})
|
||||
}
|
||||
|
||||
@@ -237,6 +237,7 @@ func (h *ssHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.H
|
||||
sniffing.WithService(h.options.Service),
|
||||
sniffing.WithDial(dial),
|
||||
sniffing.WithDialTLS(dialTLS),
|
||||
sniffing.WithBypass(h.options.Bypass),
|
||||
sniffing.WithRecorderObject(ro),
|
||||
sniffing.WithLog(log),
|
||||
)
|
||||
@@ -245,6 +246,7 @@ func (h *ssHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.H
|
||||
sniffing.WithService(h.options.Service),
|
||||
sniffing.WithDial(dial),
|
||||
sniffing.WithDialTLS(dialTLS),
|
||||
sniffing.WithBypass(h.options.Bypass),
|
||||
sniffing.WithRecorderObject(ro),
|
||||
sniffing.WithLog(log),
|
||||
)
|
||||
|
||||
@@ -209,6 +209,7 @@ func (h *forwardHandler) handleDirectForward(ctx context.Context, targetAddr str
|
||||
sniffing.WithService(h.options.Service),
|
||||
sniffing.WithDial(dial),
|
||||
sniffing.WithDialTLS(dialTLS),
|
||||
sniffing.WithBypass(h.options.Bypass),
|
||||
sniffing.WithRecorderObject(ro),
|
||||
sniffing.WithLog(log),
|
||||
)
|
||||
@@ -217,6 +218,7 @@ func (h *forwardHandler) handleDirectForward(ctx context.Context, targetAddr str
|
||||
sniffing.WithService(h.options.Service),
|
||||
sniffing.WithDial(dial),
|
||||
sniffing.WithDialTLS(dialTLS),
|
||||
sniffing.WithBypass(h.options.Bypass),
|
||||
sniffing.WithRecorderObject(ro),
|
||||
sniffing.WithLog(log),
|
||||
)
|
||||
|
||||
@@ -180,6 +180,7 @@ func (h *unixHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler
|
||||
sniffing.WithService(h.options.Service),
|
||||
sniffing.WithDial(dial),
|
||||
sniffing.WithDialTLS(dialTLS),
|
||||
sniffing.WithBypass(h.options.Bypass),
|
||||
sniffing.WithRecorderObject(ro),
|
||||
sniffing.WithLog(log),
|
||||
)
|
||||
@@ -188,6 +189,7 @@ func (h *unixHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler
|
||||
sniffing.WithService(h.options.Service),
|
||||
sniffing.WithDial(dial),
|
||||
sniffing.WithDialTLS(dialTLS),
|
||||
sniffing.WithBypass(h.options.Bypass),
|
||||
sniffing.WithRecorderObject(ro),
|
||||
sniffing.WithLog(log),
|
||||
)
|
||||
|
||||
@@ -14,7 +14,11 @@ import (
|
||||
// addresses assigned to that interface. The network parameter determines the address
|
||||
// type (TCPAddr for "tcp"/"tcp4"/"tcp6", UDPAddr for "udp"/"udp4"/"udp6", IPAddr
|
||||
// otherwise).
|
||||
func ParseInterfaceAddr(ifceName, network string) (ifce string, addr []net.Addr, err error) {
|
||||
//
|
||||
// isIP reports whether ifceName was parsed as an IP address (true) rather than an
|
||||
// interface name (false). When isIP is true, callers should skip SO_BINDTODEVICE —
|
||||
// the user's intent is source IP binding for policy routing, not device binding.
|
||||
func ParseInterfaceAddr(ifceName, network string) (ifce string, addr []net.Addr, isIP bool, err error) {
|
||||
if ifceName == "" {
|
||||
addr = append(addr, nil)
|
||||
return
|
||||
@@ -43,6 +47,8 @@ func ParseInterfaceAddr(ifceName, network string) (ifce string, addr []net.Addr,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// ifceName is an IP address — skip SO_BINDTODEVICE, use LocalAddr only
|
||||
isIP = true
|
||||
ifce, err = findInterfaceByIP(ip)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -202,39 +202,48 @@ func Test_findInterfaceByIP(t *testing.T) {
|
||||
|
||||
func TestParseInterfaceAddr(t *testing.T) {
|
||||
// Empty interface name
|
||||
ifce, addrs, err := ParseInterfaceAddr("", "tcp")
|
||||
ifce, addrs, isIP, err := ParseInterfaceAddr("", "tcp")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if ifce != "" {
|
||||
t.Errorf("expected empty ifce, got %s", ifce)
|
||||
}
|
||||
if isIP {
|
||||
t.Errorf("expected isIP=false for empty string, got true")
|
||||
}
|
||||
if len(addrs) != 1 || addrs[0] != nil {
|
||||
t.Errorf("expected [nil], got %v", addrs)
|
||||
}
|
||||
|
||||
// Non-existent interface
|
||||
_, _, err = ParseInterfaceAddr("nonexistent_interface_xyz", "tcp")
|
||||
_, _, _, err = ParseInterfaceAddr("nonexistent_interface_xyz", "tcp")
|
||||
if err == nil {
|
||||
t.Error("expected error for non-existent interface")
|
||||
}
|
||||
|
||||
// IP string as interface name
|
||||
ifce, addrs, err = ParseInterfaceAddr("127.0.0.1", "tcp")
|
||||
// IP string as interface name — isIP should be true
|
||||
ifce, addrs, isIP, err = ParseInterfaceAddr("127.0.0.1", "tcp")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if !isIP {
|
||||
t.Errorf("expected isIP=true for IP input, got false")
|
||||
}
|
||||
if ifce == "" {
|
||||
t.Log("127.0.0.1 not found on any interface (may happen in containers)")
|
||||
} else if len(addrs) != 1 {
|
||||
t.Errorf("expected 1 addr, got %d", len(addrs))
|
||||
}
|
||||
|
||||
// IP that doesn't exist on any interface
|
||||
ifce, addrs, err = ParseInterfaceAddr("203.0.113.1", "udp")
|
||||
// IP that doesn't exist on any interface — should still have isIP=true
|
||||
ifce, addrs, isIP, err = ParseInterfaceAddr("203.0.113.1", "udp")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if !isIP {
|
||||
t.Errorf("expected isIP=true for IP input, got false")
|
||||
}
|
||||
if ifce != "" {
|
||||
t.Errorf("expected empty ifce for unassigned IP, got %s", ifce)
|
||||
}
|
||||
@@ -243,10 +252,13 @@ func TestParseInterfaceAddr(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test UDP network specific returns
|
||||
ifce, addrs, err = ParseInterfaceAddr("127.0.0.1", "udp")
|
||||
ifce, addrs, isIP, err = ParseInterfaceAddr("127.0.0.1", "udp")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if !isIP {
|
||||
t.Errorf("expected isIP=true for IP input, got false")
|
||||
}
|
||||
if ifce != "" {
|
||||
if len(addrs) != 1 {
|
||||
t.Errorf("expected 1 addr, got %d", len(addrs))
|
||||
|
||||
@@ -76,13 +76,14 @@ func (d *Dialer) Dial(ctx context.Context, network, addr string) (conn net.Conn,
|
||||
ifce = strings.TrimSuffix(ifce, "!")
|
||||
var ifceName string
|
||||
var ifAddrs []net.Addr
|
||||
ifceName, ifAddrs, err = xnet.ParseInterfaceAddr(ifce, network)
|
||||
var isIP bool
|
||||
ifceName, ifAddrs, isIP, err = xnet.ParseInterfaceAddr(ifce, network)
|
||||
if err != nil && strict {
|
||||
return
|
||||
}
|
||||
|
||||
for _, ifAddr := range ifAddrs {
|
||||
conn, err = d.dialOnce(ctx, network, addr, ifceName, ifAddr, log)
|
||||
conn, err = d.dialOnce(ctx, network, addr, ifceName, ifAddr, !isIP, log)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
@@ -100,7 +101,7 @@ func (d *Dialer) Dial(ctx context.Context, network, addr string) (conn net.Conn,
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Dialer) dialOnce(ctx context.Context, network, addr, ifceName string, ifAddr net.Addr, log logger.Logger) (net.Conn, error) {
|
||||
func (d *Dialer) dialOnce(ctx context.Context, network, addr, ifceName string, ifAddr net.Addr, bindToDevice bool, log logger.Logger) (net.Conn, error) {
|
||||
if ifceName != "" {
|
||||
log.Debugf("dial %s/%s via interface %s@%s", addr, network, ifceName, ifAddr)
|
||||
}
|
||||
@@ -148,7 +149,7 @@ func (d *Dialer) dialOnce(ctx context.Context, network, addr, ifceName string, i
|
||||
LocalAddr: ifAddr,
|
||||
Control: func(network, address string, c syscall.RawConn) error {
|
||||
return c.Control(func(fd uintptr) {
|
||||
if ifceName != "" {
|
||||
if ifceName != "" && bindToDevice {
|
||||
if err := bindDevice(network, address, fd, ifceName); err != nil {
|
||||
log.Warnf("%s/%s bind device: %v", address, network, err)
|
||||
}
|
||||
|
||||
@@ -11,14 +11,6 @@ func bindDevice(network, address string, fd uintptr, ifceName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip SO_BINDTODEVICE for loopback interface — it prevents
|
||||
// outbound traffic from reaching non-local destinations.
|
||||
// Source IP binding via LocalAddr is sufficient for policy routing
|
||||
// when using IP aliases on loopback (e.g. ip addr add 10.1.1.1/32 dev lo).
|
||||
if ifce, err := net.InterfaceByName(ifceName); err == nil && ifce.Flags&net.FlagLoopback != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
host, _, _ := net.SplitHostPort(address)
|
||||
if ip := net.ParseIP(host); ip != nil && !ip.IsGlobalUnicast() {
|
||||
return nil
|
||||
|
||||
+21
-2
@@ -13,7 +13,8 @@ import (
|
||||
|
||||
// Resolve resolves addr to a concrete IP address. If hosts is non-nil, it is
|
||||
// consulted first. If r is non-nil and no host mapping matched, the resolver
|
||||
// is used. If neither is available, addr is returned unchanged.
|
||||
// is used. If neither is available, Go's native pure-Go resolver is used as a
|
||||
// fallback to avoid cgo-based DNS lookups that would block OS threads.
|
||||
func Resolve(ctx context.Context, network, addr string, r resolver.Resolver, hosts hosts.HostMapper, log logger.Logger) (string, error) {
|
||||
if addr == "" {
|
||||
return addr, nil
|
||||
@@ -51,5 +52,23 @@ func Resolve(ctx context.Context, network, addr string, r resolver.Resolver, hos
|
||||
}
|
||||
return net.JoinHostPort(ips[0].String(), port), nil
|
||||
}
|
||||
return addr, nil
|
||||
|
||||
// A literal IP address needs no resolution.
|
||||
if net.ParseIP(host) != nil {
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// Fallback to Go's native (pure-Go) resolver when no custom resolver or
|
||||
// host mapper is configured. This avoids cgo-based DNS lookups in
|
||||
// downstream net.Dialer.DialContext which would block OS threads and
|
||||
// cause thread exhaustion under high connection concurrency.
|
||||
lr := &net.Resolver{PreferGo: true}
|
||||
ips, err := lr.LookupIP(ctx, "ip", host)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return "", fmt.Errorf("resolver: domain %s does not exist", host)
|
||||
}
|
||||
return net.JoinHostPort(ips[0].String(), port), nil
|
||||
}
|
||||
|
||||
@@ -115,13 +115,18 @@ func TestResolve_ResolverEmptyResult(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolve_NoResolverNoHosts(t *testing.T) {
|
||||
// When no resolver and no host mapper are configured, the function now
|
||||
// resolves via Go's native DNS resolver (PreferGo: true). This test
|
||||
// uses an IP address which is short-circuited at the top of Resolve,
|
||||
// avoiding the need for actual DNS resolution in unit tests.
|
||||
// DNS fallback behavior is verified by the e2e tests.
|
||||
ctx := context.Background()
|
||||
addr, err := Resolve(ctx, "tcp", "example.com:8080", nil, nil, nil)
|
||||
addr, err := Resolve(ctx, "tcp", "1.2.3.4:8080", nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if addr != "example.com:8080" {
|
||||
t.Errorf("expected example.com:8080, got %s", addr)
|
||||
if addr != "1.2.3.4:8080" {
|
||||
t.Errorf("expected 1.2.3.4:8080, got %s", addr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,9 @@ func (h *Sniffer) HandleHTTP(ctx context.Context, conn net.Conn, opts ...HandleO
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cc.Close()
|
||||
defer func() { cc.Close() }()
|
||||
|
||||
upstreamHost := normalizeHost(ro.HTTP.Host, "80")
|
||||
|
||||
ho.log = log.WithFields(map[string]any{"src": cc.LocalAddr().String(), "dst": cc.RemoteAddr().String()})
|
||||
log = ho.log
|
||||
@@ -112,6 +114,41 @@ func (h *Sniffer) HandleHTTP(ctx context.Context, conn net.Conn, opts ...HandleO
|
||||
log.Trace(string(dump))
|
||||
}
|
||||
|
||||
// When DNS override directs multiple domains to the same proxy IP,
|
||||
// the browser may reuse a keep-alive connection for a different host.
|
||||
// Re-dial to ensure requests reach the correct upstream.
|
||||
if reqHost := normalizeHost(req.Host, "80"); reqHost != "" && reqHost != upstreamHost {
|
||||
cc.Close()
|
||||
newNode, res, resolveErr := resolveHTTPNode(ctx, reqHost, req, &ho)
|
||||
if resolveErr != nil {
|
||||
ro.HTTP.StatusCode = res.StatusCode
|
||||
res.Write(conn)
|
||||
return resolveErr
|
||||
}
|
||||
dial := ho.dial
|
||||
if dial == nil {
|
||||
dial = (&net.Dialer{}).DialContext
|
||||
}
|
||||
newCC, dialErr := dial(ctx, "tcp", newNode.Addr)
|
||||
if dialErr != nil {
|
||||
return dialErr
|
||||
}
|
||||
newCC = tlsWrapConn(newCC, newNode.Options().TLS)
|
||||
upstreamHost = reqHost
|
||||
node = newNode
|
||||
cc = newCC
|
||||
ro.Host = reqHost
|
||||
ro.SrcAddr = cc.LocalAddr().String()
|
||||
ro.DstAddr = cc.RemoteAddr().String()
|
||||
log = log.WithFields(map[string]any{
|
||||
"host": reqHost,
|
||||
"node": node.Name,
|
||||
"dst": node.Addr,
|
||||
"src": cc.LocalAddr().String(),
|
||||
})
|
||||
ho.log = log
|
||||
}
|
||||
|
||||
if shouldClose, err := h.httpRoundTrip(ctx, xio.NewReadWriteCloser(br, conn, conn), cc, node, req, &pStats, &ho); err != nil || shouldClose {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func (h *Sniffer) HandleTLS(ctx context.Context, conn net.Conn, opts ...HandleOp
|
||||
if ho.log != nil {
|
||||
ho.log.Debugf("no sni in clienthello from %s", conn.RemoteAddr())
|
||||
}
|
||||
return nil
|
||||
return errors.New("tls: sni is empty, closing connection")
|
||||
}
|
||||
ro.Host = host
|
||||
|
||||
@@ -321,6 +321,7 @@ func (h *Sniffer) terminateTLS(ctx context.Context, conn, cc net.Conn, clientHel
|
||||
}),
|
||||
WithHTTPKeepalive(true),
|
||||
WithNode(ho.node),
|
||||
WithBypass(ho.bypass),
|
||||
WithRecorderObject(ro),
|
||||
WithLog(log),
|
||||
}
|
||||
|
||||
@@ -93,7 +93,9 @@ func (h *Sniffer) HandleHTTP(ctx context.Context, network string, conn net.Conn,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cc.Close()
|
||||
defer func() { cc.Close() }()
|
||||
|
||||
upstreamHost := host
|
||||
|
||||
log = log.WithFields(map[string]any{"src": cc.LocalAddr().String(), "dst": cc.RemoteAddr().String()})
|
||||
|
||||
@@ -122,6 +124,26 @@ func (h *Sniffer) HandleHTTP(ctx context.Context, network string, conn net.Conn,
|
||||
log.Trace(string(dump))
|
||||
}
|
||||
|
||||
// When DNS override directs multiple domains to the same proxy IP,
|
||||
// the browser may reuse a keep-alive connection for a different host.
|
||||
// Re-dial to ensure requests reach the correct upstream.
|
||||
if reqHost := normalizeHost(req.Host, "80"); reqHost != upstreamHost {
|
||||
cc.Close()
|
||||
cc, err = dial(ctx, network, reqHost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
upstreamHost = reqHost
|
||||
ro.Host = reqHost
|
||||
ro.SrcAddr = cc.LocalAddr().String()
|
||||
ro.DstAddr = cc.RemoteAddr().String()
|
||||
log = log.WithFields(map[string]any{
|
||||
"host": reqHost,
|
||||
"src": cc.LocalAddr().String(),
|
||||
"dst": cc.RemoteAddr().String(),
|
||||
})
|
||||
}
|
||||
|
||||
if shouldClose, err := h.httpRoundTrip(ctx, xio.NewReadWriteCloser(br, conn, conn), cc, req, readTimeout, ro, &pStats, log); err != nil || shouldClose {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
@@ -52,7 +53,7 @@ func (h *Sniffer) HandleTLS(ctx context.Context, network string, conn net.Conn,
|
||||
if log != nil {
|
||||
log.Debugf("no sni in clienthello from %s", conn.RemoteAddr())
|
||||
}
|
||||
return nil
|
||||
return errors.New("tls: sni is empty, closing connection")
|
||||
}
|
||||
ro.Host = host
|
||||
|
||||
@@ -234,6 +235,7 @@ func (h *Sniffer) terminateTLS(ctx context.Context, network string, conn, cc net
|
||||
WithDialTLS(func(ctx context.Context, network, address string, cfg *tls.Config) (net.Conn, error) {
|
||||
return clientConn, nil
|
||||
}),
|
||||
WithBypass(ho.bypass),
|
||||
WithRecorderObject(ro),
|
||||
WithLog(log),
|
||||
}
|
||||
|
||||
@@ -114,6 +114,25 @@ func (s *Session) Keepalive(interval, timeout time.Duration, retries int) {
|
||||
}
|
||||
}
|
||||
|
||||
// Ping checks if the SSH session is alive by sending a keepalive request.
|
||||
// It blocks until a response is received or the timeout expires.
|
||||
// A nil error indicates the session is healthy.
|
||||
func (s *Session) Ping(timeout time.Duration) error {
|
||||
if timeout <= 0 {
|
||||
timeout = defaultKeepaliveTimeout
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
select {
|
||||
case err := <-s.ping():
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) ping() <-chan error {
|
||||
ch := make(chan error, 1)
|
||||
go func() {
|
||||
|
||||
@@ -59,6 +59,9 @@ func (l *mtcpListener) Init(md md.Metadata) (err error) {
|
||||
lc.SetMultipathTCP(true)
|
||||
l.logger.Debugf("mptcp enabled: %v", lc.MultipathTCP())
|
||||
}
|
||||
if l.md.reuseport {
|
||||
lc.Control = l.setReusePort
|
||||
}
|
||||
ln, err := lc.Listen(context.Background(), network, l.options.Addr)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
//go:build !windows
|
||||
|
||||
package mtcp
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func (l *mtcpListener) setReusePort(network, address string, c syscall.RawConn) error {
|
||||
return c.Control(func(fd uintptr) {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
|
||||
l.logger.Errorf("failed to set SO_REUSEPORT: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build windows
|
||||
|
||||
package mtcp
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func (l *mtcpListener) setReusePort(network, address string, c syscall.RawConn) error {
|
||||
return nil
|
||||
}
|
||||
@@ -13,8 +13,9 @@ const (
|
||||
)
|
||||
|
||||
type metadata struct {
|
||||
mptcp bool
|
||||
muxCfg *mux.Config
|
||||
mptcp bool
|
||||
reuseport bool
|
||||
muxCfg *mux.Config
|
||||
backlog int
|
||||
keepalive bool
|
||||
keepaliveIdle time.Duration
|
||||
@@ -24,6 +25,7 @@ type metadata struct {
|
||||
|
||||
func (l *mtcpListener) parseMetadata(md md.Metadata) (err error) {
|
||||
l.md.mptcp = mdutil.GetBool(md, "mptcp")
|
||||
l.md.reuseport = mdutil.GetBool(md, "reuseport")
|
||||
|
||||
l.md.muxCfg = &mux.Config{
|
||||
Version: mdutil.GetInt(md, "mux.version"),
|
||||
|
||||
@@ -53,7 +53,7 @@ func (l *redirectListener) Init(md md.Metadata) (err error) {
|
||||
network = "tcp4"
|
||||
}
|
||||
lc := net.ListenConfig{}
|
||||
if l.md.tproxy {
|
||||
if l.md.tproxy || l.md.reuseport {
|
||||
lc.Control = l.control
|
||||
}
|
||||
if l.md.mptcp {
|
||||
|
||||
@@ -2,8 +2,17 @@ package tcp
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func (l *redirectListener) control(network, address string, c syscall.RawConn) error {
|
||||
if l.md.reuseport {
|
||||
return c.Control(func(fd uintptr) {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
|
||||
l.logger.Errorf("failed to set SO_REUSEPORT: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,11 +8,18 @@ import (
|
||||
|
||||
func (l *redirectListener) control(network, address string, c syscall.RawConn) error {
|
||||
return c.Control(func(fd uintptr) {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_IP, unix.IP_TRANSPARENT, 1); err != nil {
|
||||
l.logger.Errorf("SetsockoptInt(SOL_IP, IP_TRANSPARENT, 1): %v", err)
|
||||
if l.md.tproxy {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_IP, unix.IP_TRANSPARENT, 1); err != nil {
|
||||
l.logger.Errorf("failed to set IP_TRANSPARENT: %v", err)
|
||||
}
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_IPV6, unix.IPV6_TRANSPARENT, 1); err != nil {
|
||||
l.logger.Errorf("failed to set IPV6_TRANSPARENT: %v", err)
|
||||
}
|
||||
}
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_IPV6, unix.IPV6_TRANSPARENT, 1); err != nil {
|
||||
l.logger.Errorf("SetsockoptInt(SOL_IPV6, IPV6_TRANSPARENT, 1): %v", err)
|
||||
if l.md.reuseport {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
|
||||
l.logger.Errorf("failed to set SO_REUSEPORT: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
//go:build !linux && !darwin
|
||||
//go:build !linux && !darwin && !windows
|
||||
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func (l *redirectListener) control(network, address string, c syscall.RawConn) error {
|
||||
return errors.New("TProxy is not available on non-linux platform")
|
||||
if l.md.tproxy {
|
||||
return errors.New("TProxy is not available on non-linux platform")
|
||||
}
|
||||
if l.md.reuseport {
|
||||
return c.Control(func(fd uintptr) {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
|
||||
l.logger.Errorf("failed to set SO_REUSEPORT: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func (l *redirectListener) control(network, address string, c syscall.RawConn) error {
|
||||
if l.md.tproxy {
|
||||
return errors.New("TProxy is not available on non-linux platform")
|
||||
}
|
||||
// SO_REUSEADDR is the default on Windows, so reuseport is a no-op.
|
||||
return nil
|
||||
}
|
||||
@@ -6,13 +6,15 @@ import (
|
||||
)
|
||||
|
||||
type metadata struct {
|
||||
tproxy bool
|
||||
mptcp bool
|
||||
tproxy bool
|
||||
mptcp bool
|
||||
reuseport bool
|
||||
}
|
||||
|
||||
func (l *redirectListener) parseMetadata(md mdata.Metadata) (err error) {
|
||||
l.md.tproxy = mdutil.GetBool(md, "tproxy")
|
||||
l.md.mptcp = mdutil.GetBool(md, "mptcp")
|
||||
l.md.reuseport = mdutil.GetBool(md, "reuseport")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
// Package stdio implements a listener that wraps os.Stdin/os.Stdout as a
|
||||
// net.Listener. It accepts exactly one connection, making it suitable for
|
||||
// SSH ProxyCommand where the parent process (SSH) pipes the transport byte
|
||||
// stream through the child process's standard I/O.
|
||||
//
|
||||
// Usage: gost -L stdio://example.com:22 -F http://proxy:8080
|
||||
package stdio
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/listener"
|
||||
"github.com/go-gost/core/logger"
|
||||
md "github.com/go-gost/core/metadata"
|
||||
"github.com/go-gost/x/registry"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.ListenerRegistry().Register("stdio", NewListener)
|
||||
}
|
||||
|
||||
type (
|
||||
stdioListener struct {
|
||||
conn net.Conn
|
||||
done chan struct{}
|
||||
once sync.Once
|
||||
accepted bool
|
||||
mu sync.Mutex
|
||||
laddr net.Addr
|
||||
logger logger.Logger
|
||||
md metadata
|
||||
options listener.Options
|
||||
}
|
||||
)
|
||||
|
||||
// NewListener creates a stdio listener. The returned listener wraps
|
||||
// os.Stdin and os.Stdout as a single-use net.Conn.
|
||||
func NewListener(opts ...listener.Option) listener.Listener {
|
||||
options := listener.Options{}
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
return &stdioListener{
|
||||
logger: options.Logger,
|
||||
options: options,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *stdioListener) Init(md md.Metadata) (err error) {
|
||||
if err = l.parseMetadata(md); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
l.laddr = &stdioAddr{addr: "stdio"}
|
||||
l.done = make(chan struct{})
|
||||
l.conn = &stdioConn{l: l}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Accept returns the stdio connection on the first call.
|
||||
// Subsequent calls block until the listener is closed.
|
||||
func (l *stdioListener) Accept() (net.Conn, error) {
|
||||
l.mu.Lock()
|
||||
if !l.accepted {
|
||||
l.accepted = true
|
||||
l.mu.Unlock()
|
||||
return l.conn, nil
|
||||
}
|
||||
l.mu.Unlock()
|
||||
|
||||
<-l.done
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
|
||||
func (l *stdioListener) Addr() net.Addr {
|
||||
return l.laddr
|
||||
}
|
||||
|
||||
func (l *stdioListener) Close() error {
|
||||
l.close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// close signals listener completion. Idempotent and safe for concurrent use.
|
||||
func (l *stdioListener) close() {
|
||||
l.once.Do(func() { close(l.done) })
|
||||
}
|
||||
|
||||
// stdioConn implements net.Conn over os.Stdin and os.Stdout.
|
||||
type stdioConn struct {
|
||||
l *stdioListener
|
||||
}
|
||||
|
||||
func (c *stdioConn) Read(b []byte) (int, error) { return os.Stdin.Read(b) }
|
||||
func (c *stdioConn) Write(b []byte) (int, error) { return os.Stdout.Write(b) }
|
||||
|
||||
// Close terminates the process.
|
||||
//
|
||||
// A stdio listener serves exactly one connection: the process's own
|
||||
// stdin/stdout, piped from a parent such as SSH's ProxyCommand. When that
|
||||
// connection ends the parent has closed the pipe and there is nothing left
|
||||
// to serve, so the whole process must exit.
|
||||
//
|
||||
// Closing the listener alone is not enough: the service's accept loop runs
|
||||
// in a fire-and-forget goroutine (see x/service.Serve) and the process
|
||||
// blocks on the go-svc signal wait (see gost/cmd/gost/main.go), so it would
|
||||
// linger forever after the parent quits. os.Exit is the only reliable way
|
||||
// to terminate here. The handler's deferred stats/recording/logging already
|
||||
// ran before this Close (LIFO order), so nothing is lost.
|
||||
func (c *stdioConn) Close() error {
|
||||
c.l.close()
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *stdioConn) LocalAddr() net.Addr { return &stdioAddr{addr: "stdio"} }
|
||||
func (c *stdioConn) RemoteAddr() net.Addr { return &stdioAddr{addr: "pipe"} }
|
||||
|
||||
// Deadline methods are no-ops: os.Stdin/os.Stdout do not support
|
||||
// deadline-based I/O.
|
||||
func (c *stdioConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (c *stdioConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (c *stdioConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
|
||||
// stdioAddr is a trivial net.Addr for stdio connections.
|
||||
type stdioAddr struct {
|
||||
addr string
|
||||
}
|
||||
|
||||
func (a *stdioAddr) Network() string { return "tcp" }
|
||||
func (a *stdioAddr) String() string { return a.addr }
|
||||
@@ -0,0 +1,13 @@
|
||||
package stdio
|
||||
|
||||
import (
|
||||
md "github.com/go-gost/core/metadata"
|
||||
)
|
||||
|
||||
type metadata struct {
|
||||
// reserved for future options (e.g., timeouts)
|
||||
}
|
||||
|
||||
func (l *stdioListener) parseMetadata(md md.Metadata) (err error) {
|
||||
return
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package tap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
@@ -68,7 +69,13 @@ func (l *tapListener) Init(md mdata.Metadata) (err error) {
|
||||
func (l *tapListener) listenLoop() {
|
||||
for {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
err := func() error {
|
||||
err := func() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic in createTap: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ifce, name, ip, err := l.createTap()
|
||||
if err != nil {
|
||||
if ifce != nil {
|
||||
|
||||
@@ -56,6 +56,9 @@ func (l *tcpListener) Init(md md.Metadata) (err error) {
|
||||
lc.SetMultipathTCP(true)
|
||||
l.logger.Debugf("mptcp enabled: %v", lc.MultipathTCP())
|
||||
}
|
||||
if l.md.reuseport {
|
||||
lc.Control = l.setReusePort
|
||||
}
|
||||
ln, err := lc.Listen(context.Background(), network, l.options.Addr)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
//go:build !windows
|
||||
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func (l *tcpListener) setReusePort(network, address string, c syscall.RawConn) error {
|
||||
return c.Control(func(fd uintptr) {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
|
||||
l.logger.Errorf("failed to set SO_REUSEPORT: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build windows
|
||||
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func (l *tcpListener) setReusePort(network, address string, c syscall.RawConn) error {
|
||||
return nil
|
||||
}
|
||||
@@ -8,7 +8,8 @@ import (
|
||||
)
|
||||
|
||||
type metadata struct {
|
||||
mptcp bool
|
||||
mptcp bool
|
||||
reuseport bool
|
||||
|
||||
keepalive bool
|
||||
keepaliveIdle time.Duration
|
||||
@@ -18,6 +19,7 @@ type metadata struct {
|
||||
|
||||
func (l *tcpListener) parseMetadata(md md.Metadata) (err error) {
|
||||
l.md.mptcp = mdutil.GetBool(md, "mptcp")
|
||||
l.md.reuseport = mdutil.GetBool(md, "reuseport")
|
||||
|
||||
l.md.keepalive = mdutil.GetBool(md, "keepalive")
|
||||
l.md.keepaliveIdle = mdutil.GetDuration(md, "keepalive.idle")
|
||||
|
||||
@@ -2,6 +2,7 @@ package tun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
@@ -75,7 +76,13 @@ func (l *tunListener) Init(md mdata.Metadata) (err error) {
|
||||
func (l *tunListener) listenLoop(ready context.CancelCauseFunc) {
|
||||
for {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
err := func() error {
|
||||
err := func() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic in createTun: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ifce, name, ip, err := l.createTun()
|
||||
if err != nil {
|
||||
if ifce != nil {
|
||||
|
||||
@@ -52,8 +52,17 @@ func (l *tunListener) createTun() (ifce io.ReadWriteCloser, name string, ip net.
|
||||
}
|
||||
}
|
||||
|
||||
if len(l.md.config.Net) > 0 {
|
||||
ipNet := l.md.config.Net[0]
|
||||
// Loop over every configured address so dual-stack (IPv4+IPv6) setups
|
||||
// install both addresses, selecting the netsh context by family: "ip"
|
||||
// for IPv4, "ipv6" for IPv6. A failing address is logged and skipped so
|
||||
// one bad entry doesn't prevent the rest from being applied; the first
|
||||
// successfully installed address is reported as the interface IP.
|
||||
//
|
||||
// Caveat: unlike Linux's netlink.AddrAdd, "netsh interface ip set
|
||||
// address" replaces rather than adds, so only a single IPv4 address is
|
||||
// supported (one IPv4 + one IPv6 works; multiple IPv4 addresses
|
||||
// silently overwrite).
|
||||
for _, ipNet := range l.md.config.Net {
|
||||
cmd := fmt.Sprintf("netsh interface ip set address name=%s "+
|
||||
"source=static addr=%s mask=%s gateway=none",
|
||||
name, ipNet.IP.String(), ipMask(ipNet.Mask))
|
||||
@@ -65,10 +74,19 @@ func (l *tunListener) createTun() (ifce io.ReadWriteCloser, name string, ip net.
|
||||
|
||||
args := strings.Split(cmd, " ")
|
||||
if er := exec.Command(args[0], args[1:]...).Run(); er != nil {
|
||||
err = fmt.Errorf("%s: %v", cmd, er)
|
||||
return
|
||||
l.log.Errorf("%s: %v", cmd, er)
|
||||
continue
|
||||
}
|
||||
ip = ipNet.IP
|
||||
if ip == nil {
|
||||
ip = ipNet.IP
|
||||
}
|
||||
}
|
||||
|
||||
// If not a single address could be configured, surface an error rather
|
||||
// than returning a nil IP (and thus a nil peer address) to the caller.
|
||||
if ip == nil && len(l.md.config.Net) > 0 {
|
||||
err = fmt.Errorf("failed to configure any address on interface %s", name)
|
||||
return
|
||||
}
|
||||
|
||||
if err = l.addRoutes(name, l.md.config.Gateway); err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package tungo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
@@ -68,7 +69,13 @@ func (l *tunListener) Init(md mdata.Metadata) (err error) {
|
||||
func (l *tunListener) listenLoop(ready context.CancelCauseFunc) {
|
||||
for {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
err := func() error {
|
||||
err := func() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic in createTun: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ifce, name, ip, err := l.createTun()
|
||||
if err != nil {
|
||||
if ifce != nil {
|
||||
|
||||
@@ -56,8 +56,17 @@ func (l *tunListener) createTun() (ifce io.ReadWriteCloser, name string, ip net.
|
||||
}
|
||||
}
|
||||
|
||||
if len(l.md.config.Net) > 0 {
|
||||
ipNet := l.md.config.Net[0]
|
||||
// Loop over every configured address so dual-stack (IPv4+IPv6) setups
|
||||
// install both addresses, selecting the netsh context by family: "ip"
|
||||
// for IPv4, "ipv6" for IPv6. A failing address is logged and skipped so
|
||||
// one bad entry doesn't prevent the rest from being applied; the first
|
||||
// successfully installed address is reported as the interface IP.
|
||||
//
|
||||
// Caveat: unlike Linux's netlink.AddrAdd, "netsh interface ip set
|
||||
// address" replaces rather than adds, so only a single IPv4 address is
|
||||
// supported (one IPv4 + one IPv6 works; multiple IPv4 addresses
|
||||
// silently overwrite).
|
||||
for _, ipNet := range l.md.config.Net {
|
||||
cmd := fmt.Sprintf("netsh interface ip set address name=%s "+
|
||||
"source=static addr=%s mask=%s",
|
||||
name, ipNet.IP.String(), ipMask(ipNet.Mask))
|
||||
@@ -73,10 +82,19 @@ func (l *tunListener) createTun() (ifce io.ReadWriteCloser, name string, ip net.
|
||||
l.log.Debugf("%s: %s", cmd, windows.ByteSliceToString(output))
|
||||
}
|
||||
if er != nil {
|
||||
err = fmt.Errorf("%s: %v", cmd, er)
|
||||
return
|
||||
l.log.Errorf("%s: %v", cmd, er)
|
||||
continue
|
||||
}
|
||||
ip = ipNet.IP
|
||||
if ip == nil {
|
||||
ip = ipNet.IP
|
||||
}
|
||||
}
|
||||
|
||||
// If not a single address could be configured, surface an error rather
|
||||
// than returning a nil IP (and thus a nil peer address) to the caller.
|
||||
if ip == nil && len(l.md.config.Net) > 0 {
|
||||
err = fmt.Errorf("failed to configure any address on interface %s", name)
|
||||
return
|
||||
}
|
||||
|
||||
if err = l.addRoutes(name, l.md.config.Gateway); err != nil {
|
||||
|
||||
+25
-1
@@ -2,9 +2,11 @@ package ws
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdlog "log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/limiter"
|
||||
@@ -85,6 +87,12 @@ func (l *wsListener) Init(md md.Metadata) (err error) {
|
||||
Addr: l.options.Addr,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: l.md.readHeaderTimeout,
|
||||
// Route HTTP server errors (TLS handshake failures, malformed
|
||||
// requests) to the GOST logger instead of the default stderr
|
||||
// output via log.Printf. Without this, network scanners and
|
||||
// protocol filters probing the WSS port produce uncontrolled
|
||||
// noise: "http: TLS handshake error from ...".
|
||||
ErrorLog: stdlog.New(&logWriter{log: l.log}, "", 0),
|
||||
}
|
||||
|
||||
l.cqueue = make(chan net.Conn, l.md.backlog)
|
||||
@@ -186,7 +194,9 @@ func (l *wsListener) upgrade(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
conn, err := l.upgrader.Upgrade(w, r, l.md.header)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
// Upgrade failures (plain HTTP requests, websocket
|
||||
// handshake errors) are operational noise — log at Warn.
|
||||
log.Warn(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -208,3 +218,17 @@ func (l *wsListener) upgrade(w http.ResponseWriter, r *http.Request) {
|
||||
log.Warnf("connection queue is full, client %s discarded", conn.RemoteAddr())
|
||||
}
|
||||
}
|
||||
|
||||
// logWriter adapts a GOST logger to the io.Writer interface required by
|
||||
// http.Server.ErrorLog. It writes each log entry as a Debug-level message
|
||||
// so that HTTP server errors (TLS handshake failures, malformed requests)
|
||||
// are captured in GOST's logging pipeline instead of escaping to stderr
|
||||
// via log.Printf.
|
||||
type logWriter struct {
|
||||
log logger.Logger
|
||||
}
|
||||
|
||||
func (w *logWriter) Write(p []byte) (n int, err error) {
|
||||
w.log.Debug(strings.TrimSpace(string(p)))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
+25
-2
@@ -4,6 +4,8 @@ import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/go-gost/core/metrics"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -30,10 +32,31 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
defaultMetrics metrics.Metrics = NewMetrics()
|
||||
enabled atomic.Bool
|
||||
defaultMetrics metrics.Metrics
|
||||
defaultRegistry *prometheus.Registry
|
||||
enabled atomic.Bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
defaultRegistry = prometheus.NewRegistry()
|
||||
|
||||
// Register Go and process collectors on our custom registry so the metrics
|
||||
// endpoint is self-contained. The process collector is platform-specific:
|
||||
// Linux/Windows use the standard procfs/Win32-based collector; all other
|
||||
// platforms (FreeBSD, Darwin, etc.) use a gopsutil-based collector.
|
||||
defaultRegistry.MustRegister(collectors.NewGoCollector())
|
||||
registerProcessCollector(defaultRegistry)
|
||||
|
||||
defaultMetrics = NewMetrics(defaultRegistry)
|
||||
}
|
||||
|
||||
// Registry returns the custom Prometheus registry that holds all GOST metrics
|
||||
// plus the Go and process collectors. The metrics HTTP endpoint should serve
|
||||
// from this registry rather than prometheus.DefaultGatherer.
|
||||
func Registry() *prometheus.Registry {
|
||||
return defaultRegistry
|
||||
}
|
||||
|
||||
// Enable enables or disables metrics collection globally. When disabled, all
|
||||
// GetCounter, GetGauge, and GetObserver calls return noop implementations.
|
||||
func Enable(b bool) {
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
//go:build !linux && !windows
|
||||
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
psutil "github.com/shirou/gopsutil/v3/process"
|
||||
)
|
||||
|
||||
// gopsutilProcessCollector implements prometheus.Collector using gopsutil/v3
|
||||
// for cross-platform process metrics on non-Linux, non-Windows systems
|
||||
// (FreeBSD, Darwin, etc.) where procfs is unavailable.
|
||||
type gopsutilProcessCollector struct {
|
||||
pidFn func() (int, error)
|
||||
cpuTotal *prometheus.Desc
|
||||
openFDs *prometheus.Desc
|
||||
maxFDs *prometheus.Desc
|
||||
vsize *prometheus.Desc
|
||||
maxVsize *prometheus.Desc
|
||||
rss *prometheus.Desc
|
||||
startTime *prometheus.Desc
|
||||
reportErrors bool
|
||||
}
|
||||
|
||||
func getPID() (int, error) { return os.Getpid(), nil }
|
||||
|
||||
func newGopsutilProcessCollector() prometheus.Collector {
|
||||
return &gopsutilProcessCollector{
|
||||
pidFn: getPID,
|
||||
cpuTotal: prometheus.NewDesc(
|
||||
"process_cpu_seconds_total",
|
||||
"Total user and system CPU time spent in seconds.",
|
||||
nil, nil,
|
||||
),
|
||||
openFDs: prometheus.NewDesc(
|
||||
"process_open_fds",
|
||||
"Number of open file descriptors.",
|
||||
nil, nil,
|
||||
),
|
||||
maxFDs: prometheus.NewDesc(
|
||||
"process_max_fds",
|
||||
"Maximum number of open file descriptors.",
|
||||
nil, nil,
|
||||
),
|
||||
vsize: prometheus.NewDesc(
|
||||
"process_virtual_memory_bytes",
|
||||
"Virtual memory size in bytes.",
|
||||
nil, nil,
|
||||
),
|
||||
maxVsize: prometheus.NewDesc(
|
||||
"process_virtual_memory_max_bytes",
|
||||
"Maximum amount of virtual memory available in bytes.",
|
||||
nil, nil,
|
||||
),
|
||||
rss: prometheus.NewDesc(
|
||||
"process_resident_memory_bytes",
|
||||
"Resident memory size in bytes.",
|
||||
nil, nil,
|
||||
),
|
||||
startTime: prometheus.NewDesc(
|
||||
"process_start_time_seconds",
|
||||
"Start time of the process since unix epoch in seconds.",
|
||||
nil, nil,
|
||||
),
|
||||
reportErrors: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *gopsutilProcessCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- c.cpuTotal
|
||||
ch <- c.openFDs
|
||||
ch <- c.maxFDs
|
||||
ch <- c.vsize
|
||||
ch <- c.maxVsize
|
||||
ch <- c.rss
|
||||
ch <- c.startTime
|
||||
}
|
||||
|
||||
func (c *gopsutilProcessCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
pid, err := c.pidFn()
|
||||
if err != nil {
|
||||
c.reportError(ch, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
p, err := psutil.NewProcess(int32(pid))
|
||||
if err != nil {
|
||||
c.reportError(ch, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Memory metrics: RSS (resident) and VMS (virtual).
|
||||
if mem, err := p.MemoryInfo(); err == nil {
|
||||
ch <- prometheus.MustNewConstMetric(c.rss, prometheus.GaugeValue, float64(mem.RSS))
|
||||
ch <- prometheus.MustNewConstMetric(c.vsize, prometheus.GaugeValue, float64(mem.VMS))
|
||||
} else {
|
||||
c.reportError(ch, c.rss, err)
|
||||
}
|
||||
|
||||
// CPU time.
|
||||
if times, err := p.Times(); err == nil {
|
||||
cpuSeconds := times.User + times.System
|
||||
ch <- prometheus.MustNewConstMetric(c.cpuTotal, prometheus.CounterValue, cpuSeconds)
|
||||
} else {
|
||||
c.reportError(ch, c.cpuTotal, err)
|
||||
}
|
||||
|
||||
// Open file descriptors.
|
||||
if fds, err := p.NumFDs(); err == nil {
|
||||
ch <- prometheus.MustNewConstMetric(c.openFDs, prometheus.GaugeValue, float64(fds))
|
||||
} else {
|
||||
c.reportError(ch, c.openFDs, err)
|
||||
}
|
||||
|
||||
// Max file descriptors — not reliably available via gopsutil on all platforms.
|
||||
// Try RLIMIT_NOFILE via gopsutil's RlimitUsage.
|
||||
if rlimits, err := p.RlimitUsage(false); err == nil && len(rlimits) > 0 {
|
||||
for _, rl := range rlimits {
|
||||
switch rl.Resource {
|
||||
case psutil.RLIMIT_NOFILE:
|
||||
ch <- prometheus.MustNewConstMetric(c.maxFDs, prometheus.GaugeValue, float64(rl.Hard))
|
||||
case psutil.RLIMIT_AS:
|
||||
ch <- prometheus.MustNewConstMetric(c.maxVsize, prometheus.GaugeValue, float64(rl.Hard))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process start time.
|
||||
if ct, err := p.CreateTime(); err == nil {
|
||||
createTime := time.Unix(ct/1000, (ct%1000)*1e6)
|
||||
ch <- prometheus.MustNewConstMetric(c.startTime, prometheus.GaugeValue, float64(createTime.Unix()))
|
||||
} else {
|
||||
c.reportError(ch, c.startTime, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *gopsutilProcessCollector) reportError(ch chan<- prometheus.Metric, desc *prometheus.Desc, err error) {
|
||||
if !c.reportErrors {
|
||||
return
|
||||
}
|
||||
if desc == nil {
|
||||
desc = prometheus.NewInvalidDesc(err)
|
||||
}
|
||||
ch <- prometheus.NewInvalidMetric(desc, err)
|
||||
}
|
||||
|
||||
// registerProcessCollector registers a gopsutil-based process metrics collector
|
||||
// for platforms where procfs is unavailable.
|
||||
func registerProcessCollector(reg *prometheus.Registry) {
|
||||
reg.MustRegister(newGopsutilProcessCollector())
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//go:build linux || windows
|
||||
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||
)
|
||||
|
||||
// registerProcessCollector registers the standard prometheus ProcessCollector
|
||||
// which uses procfs on Linux and Win32 syscalls on Windows.
|
||||
func registerProcessCollector(reg *prometheus.Registry) {
|
||||
reg.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
|
||||
}
|
||||
+10
-5
@@ -16,8 +16,13 @@ type promMetrics struct {
|
||||
}
|
||||
|
||||
// NewMetrics returns a Prometheus-based Metrics implementation. All metrics are
|
||||
// automatically registered with the default Prometheus registry.
|
||||
func NewMetrics() metrics.Metrics {
|
||||
// registered with the given registry. Use nil to register with the default
|
||||
// Prometheus registry.
|
||||
func NewMetrics(reg *prometheus.Registry) metrics.Metrics {
|
||||
if reg == nil {
|
||||
reg = prometheus.DefaultRegisterer.(*prometheus.Registry)
|
||||
}
|
||||
|
||||
host, _ := os.Hostname()
|
||||
m := &promMetrics{
|
||||
host: host,
|
||||
@@ -95,13 +100,13 @@ func NewMetrics() metrics.Metrics {
|
||||
},
|
||||
}
|
||||
for k := range m.gauges {
|
||||
prometheus.MustRegister(m.gauges[k])
|
||||
reg.MustRegister(m.gauges[k])
|
||||
}
|
||||
for k := range m.counters {
|
||||
prometheus.MustRegister(m.counters[k])
|
||||
reg.MustRegister(m.counters[k])
|
||||
}
|
||||
for k := range m.histograms {
|
||||
prometheus.MustRegister(m.histograms[k])
|
||||
reg.MustRegister(m.histograms[k])
|
||||
}
|
||||
|
||||
return m
|
||||
|
||||
@@ -7,7 +7,10 @@ import (
|
||||
|
||||
"github.com/go-gost/core/auth"
|
||||
"github.com/go-gost/core/service"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
|
||||
xmetrics "github.com/go-gost/x/metrics"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -46,6 +49,8 @@ type metricService struct {
|
||||
}
|
||||
|
||||
// NewService creates a metrics Service that exposes Prometheus metrics over HTTP.
|
||||
// It serves from the GOST custom registry rather than the default one so that
|
||||
// process metrics are available on all platforms.
|
||||
func NewService(network, addr string, opts ...Option) (service.Service, error) {
|
||||
if network == "" {
|
||||
network = "tcp"
|
||||
@@ -82,7 +87,12 @@ func NewService(network, addr string, opts ...Option) (service.Service, error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
promhttp.Handler().ServeHTTP(w, r)
|
||||
|
||||
reg := xmetrics.Registry()
|
||||
if reg == nil {
|
||||
reg = prometheus.DefaultRegisterer.(*prometheus.Registry)
|
||||
}
|
||||
promhttp.HandlerFor(reg, promhttp.HandlerOpts{}).ServeHTTP(w, r)
|
||||
}))
|
||||
return &metricService{
|
||||
s: &http.Server{
|
||||
|
||||
@@ -2,6 +2,8 @@ package wrapper
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
xmetrics "github.com/go-gost/x/metrics"
|
||||
)
|
||||
|
||||
type listener struct {
|
||||
@@ -10,10 +12,11 @@ type listener struct {
|
||||
}
|
||||
|
||||
// WrapListener wraps a net.Listener so that all accepted connections are
|
||||
// automatically wrapped with WrapConn for metrics tracking. If ln is nil,
|
||||
// nil is returned.
|
||||
// automatically wrapped with WrapConn for metrics tracking. If metrics are
|
||||
// not enabled, ln is returned unchanged to allow splice(2) optimization
|
||||
// on bare *net.TCPConn. If ln is nil, nil is returned.
|
||||
func WrapListener(service string, ln net.Listener) net.Listener {
|
||||
if ln == nil {
|
||||
if ln == nil || !xmetrics.IsEnabled() {
|
||||
return ln
|
||||
}
|
||||
return &listener{
|
||||
|
||||
@@ -10,6 +10,23 @@ import (
|
||||
xctx "github.com/go-gost/x/ctx"
|
||||
)
|
||||
|
||||
// MetadataRecorder wraps a Recorder and automatically appends metadata
|
||||
// to every Record call.
|
||||
type MetadataRecorder struct {
|
||||
recorder.Recorder
|
||||
Metadata any
|
||||
}
|
||||
|
||||
func (r *MetadataRecorder) Record(ctx context.Context, b []byte, opts ...recorder.RecordOption) error {
|
||||
if r.Recorder == nil {
|
||||
return nil
|
||||
}
|
||||
if r.Metadata == nil {
|
||||
return r.Recorder.Record(ctx, b, opts...)
|
||||
}
|
||||
return r.Recorder.Record(ctx, b, append(opts, recorder.MetadataRecordOption(r.Metadata))...)
|
||||
}
|
||||
|
||||
const (
|
||||
RecorderServiceHandler = "recorder.service.handler"
|
||||
RecorderServiceHandlerSerial = "recorder.service.handler.serial"
|
||||
|
||||
Reference in New Issue
Block a user