mirror of
https://github.com/gravitational/teleport
synced 2024-10-21 01:34:01 +00:00
Added HTTP CONNECT tunneling support for Trusted Clusters.
This commit is contained in:
parent
14cf169dc2
commit
2117306774
|
@ -34,6 +34,14 @@ const (
|
|||
SSHSessionID = "SSH_SESSION_ID"
|
||||
)
|
||||
|
||||
const (
|
||||
// HTTPSProxy is an environment variable pointing to a HTTPS proxy.
|
||||
HTTPSProxy = "HTTPS_PROXY"
|
||||
|
||||
// HTTPProxy is an environment variable pointing to a HTTP proxy.
|
||||
HTTPProxy = "HTTP_PROXY"
|
||||
)
|
||||
|
||||
const (
|
||||
// TOTPValidityPeriod is the number of seconds a TOTP token is valid.
|
||||
TOTPValidityPeriod uint = 30
|
||||
|
|
|
@ -909,6 +909,27 @@ $ ssh-keygen -L -f ~/.tsh/keys/localhost/jsmith.cert
|
|||
permit-pty
|
||||
```
|
||||
|
||||
### HTTP CONNECT Tunneling
|
||||
|
||||
Some networks funnel all connections through a proxy server where they can be
|
||||
audited and access control rules applied. For these scenarios Teleport supports
|
||||
HTTP CONNECT tunneling.
|
||||
|
||||
To use HTTP CONNECT tunneling, simply set either the `HTTPS_PROXY` or
|
||||
`HTTP_PROXY` environment variables and when Teleport builds and establishes the
|
||||
reverse tunnel to the main cluster, it will funnel all traffic though the proxy.
|
||||
Specifically Teleport will tunnel ports `3024` (SSH, reverse tunnel) and `3080`
|
||||
(HTTPS, establishing trust) through the proxy.
|
||||
|
||||
The value of `HTTPS_PROXY` or `HTTP_PROXY` should be in the format
|
||||
`scheme://host:port` where scheme is either `https` or `http`. If the
|
||||
value is `host:port`, Teleport will prepend `http`.
|
||||
|
||||
!!! tip "Note":
|
||||
`localhost` and `127.0.0.1` are invalid values for the proxy host. If for
|
||||
some reason your proxy runs locally, you'll need to provide some other DNS
|
||||
name or a private IP address for it.
|
||||
|
||||
## Using Teleport with OpenSSH
|
||||
|
||||
Teleport is a standards-compliant SSH proxy and it can work in environments with
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"net/url"
|
||||
|
||||
"github.com/gravitational/teleport"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
"github.com/gravitational/teleport/lib/httplib"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
|
||||
|
@ -219,14 +218,16 @@ func (s *AuthServer) sendValidateRequestToProxy(host string, validateRequest *Va
|
|||
log.Warn("InsecureSkipVerify used to communicate with proxy.")
|
||||
log.Warn("Make sure you intend to run Teleport in debug mode.")
|
||||
|
||||
// get the default transport (so we can get the proxy from environment)
|
||||
// but disable tls certificate checking.
|
||||
tr, ok := http.DefaultTransport.(*http.Transport)
|
||||
if !ok {
|
||||
return nil, trace.BadParameter("unable to get default transport")
|
||||
}
|
||||
tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
|
||||
insecureWebClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
// IdleConnTimeout defines the maximum amount of time before idle connections
|
||||
// are closed. Leaving this unset will lead to connections open forever and
|
||||
// will cause memory leaks in a long running process
|
||||
IdleConnTimeout: defaults.HTTPIdleTimeout,
|
||||
},
|
||||
Transport: tr,
|
||||
}
|
||||
opts = append(opts, roundtrip.HTTPClient(insecureWebClient))
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ import (
|
|||
"github.com/gravitational/teleport/lib/services"
|
||||
"github.com/gravitational/teleport/lib/sshutils"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
"github.com/gravitational/teleport/lib/utils/proxy"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/gravitational/trace"
|
||||
|
@ -153,7 +154,9 @@ func (a *Agent) connect() (conn *ssh.Client, err error) {
|
|||
return nil, trace.BadParameter("reverse tunnel cannot be created: target address is empty")
|
||||
}
|
||||
for _, authMethod := range a.authMethods {
|
||||
conn, err = ssh.Dial(a.addr.AddrNetwork, a.addr.Addr, &ssh.ClientConfig{
|
||||
// if http_proxy is set, dial through the proxy
|
||||
dialer := proxy.DialerFromEnvironment()
|
||||
conn, err = dialer.Dial(a.addr.AddrNetwork, a.addr.Addr, &ssh.ClientConfig{
|
||||
User: a.clientName,
|
||||
Auth: []ssh.AuthMethod{authMethod},
|
||||
HostKeyCallback: a.hostKeyCallback,
|
||||
|
|
162
lib/utils/proxy/proxy.go
Normal file
162
lib/utils/proxy/proxy.go
Normal file
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
Copyright 2017 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gravitational/teleport"
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
// A Dialer is a means for a client to establish a SSH connection.
|
||||
type Dialer interface {
|
||||
// Dial establishes a client connection to a SSH server.
|
||||
Dial(network string, addr string, config *ssh.ClientConfig) (*ssh.Client, error)
|
||||
}
|
||||
|
||||
type directDial struct{}
|
||||
|
||||
// Dial calls ssh.Dial directly.
|
||||
func (d directDial) Dial(network string, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {
|
||||
return ssh.Dial(network, addr, config)
|
||||
}
|
||||
|
||||
type proxyDial struct {
|
||||
proxyHost string
|
||||
}
|
||||
|
||||
// Dial first connects to a proxy, then uses the connection to establish a new
|
||||
// SSH connection.
|
||||
func (d proxyDial) Dial(network string, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {
|
||||
// build a proxy connection first
|
||||
pconn, err := dialProxy(d.proxyHost, addr)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// do the same as ssh.Dial but pass in proxy connection
|
||||
c, chans, reqs, err := ssh.NewClientConn(pconn, addr, config)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return ssh.NewClient(c, chans, reqs), nil
|
||||
}
|
||||
|
||||
// DialerFromEnvironment returns a Dial function. If the https_proxy or http_proxy
|
||||
// environment variable are set, it returns a function that will dial through
|
||||
// said proxy server. If neither variable is set, it will connect to the SSH
|
||||
// server directly.
|
||||
func DialerFromEnvironment() Dialer {
|
||||
// try and get proxy addr from the environment
|
||||
proxyAddr := getProxyAddress()
|
||||
|
||||
// if no proxy settings are in environment return regular ssh dialer,
|
||||
// otherwise return a proxy dialer
|
||||
if proxyAddr == "" {
|
||||
log.Debugf("[HTTP PROXY] No proxy set in environment, returning direct dialer.")
|
||||
return directDial{}
|
||||
}
|
||||
log.Debugf("[HTTP PROXY] Found proxy %q in environment, returning proxy dialer.", proxyAddr)
|
||||
return proxyDial{proxyHost: proxyAddr}
|
||||
}
|
||||
|
||||
func dialProxy(proxyAddr string, addr string) (net.Conn, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
var d net.Dialer
|
||||
conn, err := d.DialContext(ctx, "tcp", proxyAddr)
|
||||
if err != nil {
|
||||
log.Warnf("[HTTP PROXY] Unable to dial to proxy: %v: %v", proxyAddr, err)
|
||||
return nil, trace.ConvertSystemError(err)
|
||||
}
|
||||
|
||||
connectReq := &http.Request{
|
||||
Method: http.MethodConnect,
|
||||
URL: &url.URL{Opaque: addr},
|
||||
Host: addr,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
err = connectReq.Write(conn)
|
||||
if err != nil {
|
||||
log.Warnf("[HTTP PROXY] Unable to write to proxy: %v", err)
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
br := bufio.NewReader(conn)
|
||||
resp, err := http.ReadResponse(br, connectReq)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
log.Warnf("[HTTP PROXY] Unable to read response: %v", err)
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
f := strings.SplitN(resp.Status, " ", 2)
|
||||
conn.Close()
|
||||
return nil, trace.BadParameter("Unable to proxy connection, StatusCode %v: %v", resp.StatusCode, f[1])
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func getProxyAddress() string {
|
||||
envs := []string{
|
||||
teleport.HTTPSProxy,
|
||||
strings.ToLower(teleport.HTTPSProxy),
|
||||
teleport.HTTPProxy,
|
||||
strings.ToLower(teleport.HTTPProxy),
|
||||
}
|
||||
|
||||
for _, v := range envs {
|
||||
addr := os.Getenv(v)
|
||||
if addr != "" {
|
||||
proxyaddr, err := parse(addr)
|
||||
if err != nil {
|
||||
log.Debugf("[HTTP PROXY] Unable to parse environment variable %q: %q.", v, addr)
|
||||
continue
|
||||
}
|
||||
log.Debugf("[HTTP PROXY] Successfully parsed environment variable %q: %q to %q", v, addr, proxyaddr)
|
||||
return proxyaddr
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("[HTTP PROXY] No valid environment variables found.")
|
||||
return ""
|
||||
}
|
||||
|
||||
// parse will extract the host:port of the proxy to dial to. If the
|
||||
// value is not prefixed by "http", then it will prepend "http" and try.
|
||||
func parse(addr string) (string, error) {
|
||||
proxyurl, err := url.Parse(addr)
|
||||
if err != nil || !strings.HasPrefix(proxyurl.Scheme, "http") {
|
||||
proxyurl, err = url.Parse("http://" + addr)
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return proxyurl.Host, nil
|
||||
}
|
372
lib/utils/proxy/proxy_test.go
Normal file
372
lib/utils/proxy/proxy_test.go
Normal file
|
@ -0,0 +1,372 @@
|
|||
/*
|
||||
Copyright 2017 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) { check.TestingT(t) }
|
||||
|
||||
type ProxySuite struct{}
|
||||
|
||||
var _ = check.Suite(&ProxySuite{})
|
||||
var _ = fmt.Printf
|
||||
|
||||
func (s *ProxySuite) SetUpSuite(c *check.C) {
|
||||
utils.InitLoggerForTests()
|
||||
}
|
||||
func (s *ProxySuite) TearDownSuite(c *check.C) {}
|
||||
func (s *ProxySuite) SetUpTest(c *check.C) {}
|
||||
func (s *ProxySuite) TearDownTest(c *check.C) {}
|
||||
|
||||
func (s *ProxySuite) TestDirectDial(c *check.C) {
|
||||
os.Unsetenv("https_proxy")
|
||||
os.Unsetenv("http_proxy")
|
||||
|
||||
d := debugServer{}
|
||||
err := d.Start()
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
dialer := DialerFromEnvironment()
|
||||
client, err := dialer.Dial("tcp", d.Address(), &ssh.ClientConfig{})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
session, err := client.NewSession()
|
||||
c.Assert(err, check.IsNil)
|
||||
defer session.Close()
|
||||
|
||||
session.Run("date")
|
||||
session.Close()
|
||||
client.Close()
|
||||
|
||||
c.Assert(d.Commands(), check.DeepEquals, []string{"date"})
|
||||
}
|
||||
|
||||
func (s *ProxySuite) TestProxyDial(c *check.C) {
|
||||
dh := &debugHandler{}
|
||||
ts := httptest.NewServer(dh)
|
||||
defer ts.Close()
|
||||
|
||||
u, err := url.Parse(ts.URL)
|
||||
c.Assert(err, check.IsNil)
|
||||
os.Setenv("http_proxy", u.Host)
|
||||
|
||||
ds := debugServer{}
|
||||
err = ds.Start()
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
dialer := DialerFromEnvironment()
|
||||
client, err := dialer.Dial("tcp", ds.Address(), &ssh.ClientConfig{})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
session, err := client.NewSession()
|
||||
c.Assert(err, check.IsNil)
|
||||
defer session.Close()
|
||||
|
||||
session.Run("date")
|
||||
session.Close()
|
||||
client.Close()
|
||||
|
||||
c.Assert(ds.Commands(), check.DeepEquals, []string{"date"})
|
||||
c.Assert(dh.Count(), check.Equals, 1)
|
||||
}
|
||||
|
||||
func (s *ProxySuite) TestGetProxyAddress(c *check.C) {
|
||||
var tests = []struct {
|
||||
inEnvName string
|
||||
inEnvValue string
|
||||
outProxyAddr string
|
||||
}{
|
||||
// 0 - valid, can be raw host:port
|
||||
{
|
||||
"http_proxy",
|
||||
"proxy:1234",
|
||||
"proxy:1234",
|
||||
},
|
||||
// 1 - valid, raw host:port works for https
|
||||
{
|
||||
"HTTPS_PROXY",
|
||||
"proxy:1234",
|
||||
"proxy:1234",
|
||||
},
|
||||
// 2 - valid, correct full url
|
||||
{
|
||||
"https_proxy",
|
||||
"https://proxy:1234",
|
||||
"proxy:1234",
|
||||
},
|
||||
// 3 - valid, http endpoint can be set in https_proxy
|
||||
{
|
||||
"https_proxy",
|
||||
"http://proxy:1234",
|
||||
"proxy:1234",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
comment := check.Commentf("Test %v", i)
|
||||
|
||||
unsetEnv()
|
||||
os.Setenv(tt.inEnvName, tt.inEnvValue)
|
||||
p := getProxyAddress()
|
||||
unsetEnv()
|
||||
|
||||
c.Assert(p, check.Equals, tt.outProxyAddr, comment)
|
||||
}
|
||||
}
|
||||
|
||||
type debugServer struct {
|
||||
sync.Mutex
|
||||
|
||||
addr string
|
||||
commands []string
|
||||
}
|
||||
|
||||
func (d *debugServer) Start() error {
|
||||
hostkey, err := d.generateHostKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
freePorts, err := utils.GetFreeTCPPorts(10)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srvPort := freePorts[len(freePorts)-1]
|
||||
d.addr = "127.0.0.1:" + srvPort
|
||||
|
||||
config := &ssh.ServerConfig{
|
||||
NoClientAuth: true,
|
||||
}
|
||||
config.AddHostKey(hostkey)
|
||||
|
||||
listener, err := net.Listen("tcp", d.addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
log.Debugf("Unable to accept: %v", err)
|
||||
}
|
||||
|
||||
go d.handleConnection(conn, config)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *debugServer) handleConnection(conn net.Conn, config *ssh.ServerConfig) error {
|
||||
sconn, chans, reqs, err := ssh.NewServerConn(conn, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go ssh.DiscardRequests(reqs)
|
||||
|
||||
newchan := <-chans
|
||||
channel, requests, err := newchan.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := <-requests
|
||||
err = d.handleRequest(channel, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
channel.Close()
|
||||
sconn.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *debugServer) handleRequest(channel ssh.Channel, req *ssh.Request) error {
|
||||
if req.Type != "exec" {
|
||||
req.Reply(false, nil)
|
||||
return trace.BadParameter("only exec type supported")
|
||||
}
|
||||
|
||||
type execRequest struct {
|
||||
Command string
|
||||
}
|
||||
|
||||
var e execRequest
|
||||
if err := ssh.Unmarshal(req.Payload, &e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := exec.Command(e.Command).Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
io.Copy(channel, bytes.NewReader(out))
|
||||
channel.Close()
|
||||
|
||||
d.Lock()
|
||||
d.commands = append(d.commands, e.Command)
|
||||
d.Unlock()
|
||||
|
||||
if req.WantReply {
|
||||
req.Reply(true, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *debugServer) generateHostKey() (ssh.Signer, error) {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privateKeyPEM := &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
|
||||
}
|
||||
var privateKeyBuffer bytes.Buffer
|
||||
err = pem.Encode(&privateKeyBuffer, privateKeyPEM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hostkey, err := ssh.ParsePrivateKey(privateKeyBuffer.Bytes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return hostkey, nil
|
||||
}
|
||||
|
||||
func (d *debugServer) Commands() []string {
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
return d.commands
|
||||
}
|
||||
|
||||
func (d *debugServer) Address() string {
|
||||
return d.addr
|
||||
}
|
||||
|
||||
type debugHandler struct {
|
||||
sync.Mutex
|
||||
|
||||
count int
|
||||
}
|
||||
|
||||
func (d *debugHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// validate http connect parameters
|
||||
if r.Method != http.MethodConnect {
|
||||
http.Error(w, fmt.Sprintf("%v not supported", r.Method), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if r.Host == "" {
|
||||
http.Error(w, fmt.Sprintf("host not set"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// hijack request so we can get underlying connection
|
||||
hj, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
http.Error(w, "unable to hijack connection", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
sconn, _, err := hj.Hijack()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// dial to host we want to proxy connection to
|
||||
dconn, err := net.Dial("tcp", r.Host)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// write 200 OK to the source, but don't close the connection
|
||||
resp := &http.Response{
|
||||
Status: "OK",
|
||||
StatusCode: 200,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 0,
|
||||
}
|
||||
resp.Write(sconn)
|
||||
|
||||
// copy from src to dst and dst to src
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
io.Copy(sconn, dconn)
|
||||
done <- true
|
||||
}()
|
||||
go func() {
|
||||
io.Copy(dconn, sconn)
|
||||
done <- true
|
||||
}()
|
||||
|
||||
d.Lock()
|
||||
d.count = d.count + 1
|
||||
d.Unlock()
|
||||
|
||||
// wait until done
|
||||
<-done
|
||||
<-done
|
||||
|
||||
// close the connections
|
||||
sconn.Close()
|
||||
dconn.Close()
|
||||
}
|
||||
|
||||
func (d *debugHandler) Count() int {
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
return d.count
|
||||
}
|
||||
|
||||
func unsetEnv() {
|
||||
os.Unsetenv("http_proxy")
|
||||
os.Unsetenv("HTTP_PROXY")
|
||||
os.Unsetenv("https_proxy")
|
||||
os.Unsetenv("HTTPS_PROXY")
|
||||
}
|
Loading…
Reference in a new issue