diff --git a/Dockerfile b/Dockerfile index df7524c..0dd7869 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,15 +4,9 @@ WORKDIR /go/src/github.com/Snawoot/opera-proxy COPY . . ARG TARGETOS TARGETARCH RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -a -tags netgo -ldflags '-s -w -extldflags "-static"' -ADD https://curl.haxx.se/ca/cacert.pem /certs.crt -RUN chmod 0644 /certs.crt - -FROM scratch AS arrange -COPY --from=build /go/src/github.com/Snawoot/opera-proxy/opera-proxy / -COPY --from=build /certs.crt /etc/ssl/certs/ca-certificates.crt FROM scratch -COPY --from=arrange / / +COPY --from=build /go/src/github.com/Snawoot/opera-proxy/opera-proxy / USER 9999:9999 EXPOSE 18080/tcp ENTRYPOINT ["/opera-proxy", "-bind-address", "0.0.0.0:18080"] diff --git a/README.md b/README.md index 88cb0f8..25c510e 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,6 @@ eu3.sec-tunnel.com,77.111.244.22,443 | bind-address | String | proxy listen address (default "127.0.0.1:18080") | | bootstrap-dns | String | Comma-separated list of DNS/DoH/DoT resolvers for initial discovery of SurfEasy API address. Supported schemes are: `dns://`, `https://`, `tls://`, `tcp://`. Examples: `https://1.1.1.1/dns-query`, `tls://9.9.9.9:853` (default `https://1.1.1.3/dns-query,https://8.8.8.8/dns-query,https://dns.google/dns-query,https://security.cloudflare-dns.com/dns-query,https://fidelity.vm-0.com/q,https://wikimedia-dns.org/dns-query,https://dns.adguard-dns.com/dns-query,https://dns.quad9.net/dns-query,https://doh.cleanbrowsing.org/doh/adult-filter/`) | | cafile | String | use custom CA certificate bundle file | -| certchain-workaround | Boolean | add bundled cross-signed intermediate cert to certchain to make it check out on old systems (default true) | | config | String | read configuration from file with space-separated keys and values | | country | String | desired proxy location (default "EU") | | dp-export | - | export configuration for dumbproxy | diff --git a/dialer/upstream.go b/dialer/upstream.go index aba7226..56eed3c 100644 --- a/dialer/upstream.go +++ b/dialer/upstream.go @@ -7,7 +7,6 @@ import ( "crypto/tls" "crypto/x509" "encoding/base64" - "encoding/pem" "errors" "fmt" "io" @@ -22,35 +21,8 @@ const ( PROXY_CONNECT_METHOD = "CONNECT" PROXY_HOST_HEADER = "Host" PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization" - MISSING_CHAIN_CERT = `-----BEGIN CERTIFICATE----- -MIID0zCCArugAwIBAgIQVmcdBOpPmUxvEIFHWdJ1lDANBgkqhkiG9w0BAQwFADB7 -MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD -VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE -AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTE5MDMxMjAwMDAwMFoXDTI4 -MTIzMTIzNTk1OVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5 -MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO -ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgRUNDIENlcnRpZmljYXRpb24gQXV0 -aG9yaXR5MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEGqxUWqn5aCPnetUkb1PGWthL -q8bVttHmc3Gu3ZzWDGH926CJA7gFFOxXzu5dP+Ihs8731Ip54KODfi2X0GHE8Znc -JZFjq38wo7Rw4sehM5zzvy5cU7Ffs30yf4o043l5o4HyMIHvMB8GA1UdIwQYMBaA -FKARCiM+lvEH7OKvKe+CpX/QMKS0MB0GA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1 -xmNjmjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zARBgNVHSAECjAI -MAYGBFUdIAAwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5j -b20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQG -CCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQEM -BQADggEBABns652JLCALBIAdGN5CmXKZFjK9Dpx1WywV4ilAbe7/ctvbq5AfjJXy -ij0IckKJUAfiORVsAYfZFhr1wHUrxeZWEQff2Ji8fJ8ZOd+LygBkc7xGEJuTI42+ -FsMuCIKchjN0djsoTI0DQoWz4rIjQtUfenVqGtF8qmchxDM6OW1TyaLtYiKou+JV -bJlsQ2uRl9EMC5MCHdK8aXdJ5htN978UeAOwproLtOGFfy/cQjutdAFI3tZs4RmY -CV4Ks2dH/hzg1cEo70qLRDEmBDeNiXQ2Lu+lIg+DdEmSx/cQwgwp+7e9un/jX9Wf -8qn0dNW44bOwgeThpWOjzOoEeJBuv/c= ------END CERTIFICATE----- -` ) -var missingLinkDER, _ = pem.Decode([]byte(MISSING_CHAIN_CERT)) -var missingLink, _ = x509.ParseCertificate(missingLinkDER.Bytes) - type stringCb = func() (string, error) type Dialer interface { @@ -63,24 +35,22 @@ type ContextDialer interface { } type ProxyDialer struct { - address stringCb - tlsServerName stringCb - fakeSNI stringCb - auth stringCb - next ContextDialer - intermediateWorkaround bool - caPool *x509.CertPool + address stringCb + tlsServerName stringCb + fakeSNI stringCb + auth stringCb + next ContextDialer + caPool *x509.CertPool } -func NewProxyDialer(address, tlsServerName, fakeSNI, auth stringCb, intermediateWorkaround bool, caPool *x509.CertPool, nextDialer ContextDialer) *ProxyDialer { +func NewProxyDialer(address, tlsServerName, fakeSNI, auth stringCb, caPool *x509.CertPool, nextDialer ContextDialer) *ProxyDialer { return &ProxyDialer{ - address: address, - tlsServerName: tlsServerName, - fakeSNI: fakeSNI, - auth: auth, - next: nextDialer, - intermediateWorkaround: intermediateWorkaround, - caPool: caPool, + address: address, + tlsServerName: tlsServerName, + fakeSNI: fakeSNI, + auth: auth, + next: nextDialer, + caPool: caPool, } } @@ -116,7 +86,6 @@ func ProxyDialerFromURL(u *url.URL, next ContextDialer) (*ProxyDialer, error) { WrapStringToCb(tlsServerName), WrapStringToCb(tlsServerName), auth, - false, nil, next), nil } @@ -158,16 +127,8 @@ func (d *ProxyDialer) DialContext(ctx context.Context, network, address string) Intermediates: x509.NewCertPool(), Roots: d.caPool, } - waRequired := false for _, cert := range cs.PeerCertificates[1:] { opts.Intermediates.AddCert(cert) - if d.intermediateWorkaround && !waRequired && - bytes.Compare(cert.AuthorityKeyId, missingLink.SubjectKeyId) == 0 { - waRequired = true - } - } - if waRequired { - opts.Intermediates.AddCert(missingLink) } _, err := cs.PeerCertificates[0].Verify(opts) return err diff --git a/go.mod b/go.mod index d870c75..ee6deea 100644 --- a/go.mod +++ b/go.mod @@ -12,4 +12,7 @@ require ( golang.org/x/net v0.48.0 ) -require github.com/hashicorp/errwrap v1.1.0 // indirect +require ( + github.com/hashicorp/errwrap v1.1.0 // indirect + golang.org/x/crypto/x509roots/fallback v0.0.0-20251210140736-7dacc380ba00 // indirect +) diff --git a/go.sum b/go.sum index efd2b48..f8051bc 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,9 @@ github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQ github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/things-go/go-socks5 v0.1.0 h1:4f5dz0iMQ6cA4wseFmyLmCHmg3SWJTW92ndrKS6oERg= github.com/things-go/go-socks5 v0.1.0/go.mod h1:Riabiyu52kLsla0YmJqunt1c1JEl6iXSr4bRd7swFEA= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto/x509roots/fallback v0.0.0-20251210140736-7dacc380ba00 h1:qObov2/X4yIpr98j5t6samg3mMF12Rl4taUJd1rWj+c= +golang.org/x/crypto/x509roots/fallback v0.0.0-20251210140736-7dacc380ba00/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/main.go b/main.go index 6c06a6b..7df534c 100644 --- a/main.go +++ b/main.go @@ -29,6 +29,9 @@ import ( clog "github.com/Snawoot/opera-proxy/log" "github.com/Snawoot/opera-proxy/resolver" se "github.com/Snawoot/opera-proxy/seclient" + + _ "golang.org/x/crypto/x509roots/fallback" + "golang.org/x/crypto/x509roots/fallback/bundle" ) const ( @@ -119,7 +122,6 @@ type CLIArgs struct { refreshRetry time.Duration initRetries int initRetryInterval time.Duration - certChainWorkaround bool caFile string fakeSNI string overrideProxyAddress string @@ -174,8 +176,6 @@ func parse_args() *CLIArgs { flag.DurationVar(&args.refreshRetry, "refresh-retry", 5*time.Second, "login refresh retry interval") flag.IntVar(&args.initRetries, "init-retries", 0, "number of attempts for initialization steps, zero for unlimited retry") flag.DurationVar(&args.initRetryInterval, "init-retry-interval", 5*time.Second, "delay between initialization retries") - flag.BoolVar(&args.certChainWorkaround, "certchain-workaround", true, - "add bundled cross-signed intermediate cert to certchain to make it check out on old systems") flag.StringVar(&args.caFile, "cafile", "", "use custom CA certificate bundle file") flag.StringVar(&args.fakeSNI, "fake-SNI", "", "domain name to use as SNI in communications with servers") flag.StringVar(&args.overrideProxyAddress, "override-proxy-address", "", "use fixed proxy address instead of server address returned by SurfEasy API") @@ -230,9 +230,8 @@ func run() int { KeepAlive: 30 * time.Second, } - var caPool *x509.CertPool + caPool := x509.NewCertPool() if args.caFile != "" { - caPool = x509.NewCertPool() certs, err := ioutil.ReadFile(args.caFile) if err != nil { mainLogger.Error("Can't load CA file: %v", err) @@ -242,6 +241,19 @@ func run() int { mainLogger.Error("Can't load certificates from CA file") return 15 } + } else { + for c := range bundle.Roots() { + cert, err := x509.ParseCertificate(c.Certificate) + if err != nil { + mainLogger.Error("Unable to parse bundled certificate: %v", err) + return 15 + } + if c.Constraint == nil { + caPool.AddCert(cert) + } else { + caPool.AddCertWithConstraint(cert, c.Constraint) + } + } } xproxy.RegisterDialerType("http", proxyFromURLWrapper) @@ -278,7 +290,7 @@ func run() int { mainLogger.Info("Using fixed API host address = %s", args.apiAddress) seclientDialer = dialer.NewFixedDialer(args.apiAddress, seclientDialer) } else if len(args.bootstrapDNS.values) > 0 { - resolver, err := resolver.FastFromURLs(args.bootstrapDNS.values...) + resolver, err := resolver.FastFromURLs(caPool, args.bootstrapDNS.values...) if err != nil { mainLogger.Critical("Unable to instantiate DNS resolver: %v", err) return 4 @@ -372,7 +384,6 @@ func run() int { func() (string, error) { return dialer.BasicAuthHeader(seclient.GetProxyCredentials()), nil }, - args.certChainWorkaround, caPool, d) } diff --git a/resolver/factory.go b/resolver/factory.go index d650174..8b5f7a3 100644 --- a/resolver/factory.go +++ b/resolver/factory.go @@ -1,15 +1,19 @@ package resolver import ( + "crypto/tls" + "crypto/x509" "errors" "net" + "net/http" "net/url" "strings" + "time" "github.com/ncruces/go-dns" ) -func FromURL(u string) (*net.Resolver, error) { +func FromURL(u string, caPool *x509.CertPool) (*net.Resolver, error) { begin: parsed, err := url.Parse(u) if err != nil { @@ -48,13 +52,29 @@ begin: parsed.Scheme = "https" u = parsed.String() } - return dns.NewDoHResolver(u, dns.DoHAddresses(net.JoinHostPort(host, port))) + return dns.NewDoHResolver(u, + dns.DoHAddresses(net.JoinHostPort(host, port)), + dns.DoHTransport(&http.Transport{ + MaxIdleConns: http.DefaultMaxIdleConnsPerHost, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ForceAttemptHTTP2: true, + TLSClientConfig: &tls.Config{ + RootCAs: caPool, + }, + }), + ) case "tls", "dot": if port == "" { port = "853" } hp := net.JoinHostPort(host, port) - return dns.NewDoTResolver(hp, dns.DoTAddresses(hp)) + return dns.NewDoTResolver(hp, + dns.DoTAddresses(hp), + dns.DoTConfig(&tls.Config{ + RootCAs: caPool, + }), + ) default: return nil, errors.New("not implemented") } diff --git a/resolver/fast.go b/resolver/fast.go index bb1508b..692641f 100644 --- a/resolver/fast.go +++ b/resolver/fast.go @@ -2,6 +2,7 @@ package resolver import ( "context" + "crypto/x509" "fmt" "net/netip" @@ -16,10 +17,10 @@ type FastResolver struct { upstreams []LookupNetIPer } -func FastFromURLs(urls ...string) (LookupNetIPer, error) { +func FastFromURLs(caPool *x509.CertPool, urls ...string) (LookupNetIPer, error) { resolvers := make([]LookupNetIPer, 0, len(urls)) for i, u := range urls { - res, err := FromURL(u) + res, err := FromURL(u, caPool) if err != nil { return nil, fmt.Errorf("unable to construct resolver #%d (%q): %w", i, u, err) }