mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +00:00
chore: replace cloudflare dev tunnel with frp (#867)
This commit is contained in:
92
coderd/devtunnel/tunnel.go
Normal file
92
coderd/devtunnel/tunnel.go
Normal file
@ -0,0 +1,92 @@
|
||||
package devtunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
frpclient "github.com/fatedier/frp/client"
|
||||
frpconfig "github.com/fatedier/frp/pkg/config"
|
||||
frpconsts "github.com/fatedier/frp/pkg/consts"
|
||||
frplog "github.com/fatedier/frp/pkg/util/log"
|
||||
frpcrypto "github.com/fatedier/golib/crypto"
|
||||
"github.com/google/uuid"
|
||||
"github.com/moby/moby/pkg/namesgenerator"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// New creates a new tunnel pointing at the URL provided. Once created, it
|
||||
// returns the external hostname that will resolve to it.
|
||||
//
|
||||
// The tunnel will exit when the context provided is canceled.
|
||||
//
|
||||
// Upstream connection occurs synchronously through a selfhosted
|
||||
// https://github.com/fatedier/frp instance. The error channel sends an error
|
||||
// when the frp client stops.
|
||||
func New(ctx context.Context, coderurl *url.URL) (string, <-chan error, error) {
|
||||
frpcrypto.DefaultSalt = "frp"
|
||||
|
||||
cfg := frpconfig.GetDefaultClientConf()
|
||||
cfg.ServerAddr = "frp-tunnel.coder.app"
|
||||
cfg.ServerPort = 7000
|
||||
|
||||
// Ignore all logs from frp.
|
||||
frplog.InitLog("file", "/dev/null", "error", 0, false)
|
||||
|
||||
var (
|
||||
id = uuid.NewString()
|
||||
subdomain = strings.ReplaceAll(namesgenerator.GetRandomName(1), "_", "-")
|
||||
portStr = coderurl.Port()
|
||||
)
|
||||
if portStr == "" {
|
||||
portStr = "80"
|
||||
}
|
||||
|
||||
port, err := strconv.ParseInt(portStr, 10, 64)
|
||||
if err != nil {
|
||||
return "", nil, xerrors.Errorf("parse port %q: %w", port, err)
|
||||
}
|
||||
|
||||
httpcfg := map[string]frpconfig.ProxyConf{
|
||||
id: &frpconfig.HTTPProxyConf{
|
||||
BaseProxyConf: frpconfig.BaseProxyConf{
|
||||
ProxyName: id,
|
||||
ProxyType: frpconsts.HTTPProxy,
|
||||
UseEncryption: false,
|
||||
UseCompression: false,
|
||||
LocalSvrConf: frpconfig.LocalSvrConf{
|
||||
LocalIP: coderurl.Hostname(),
|
||||
LocalPort: int(port),
|
||||
},
|
||||
},
|
||||
DomainConf: frpconfig.DomainConf{
|
||||
SubDomain: subdomain,
|
||||
},
|
||||
Locations: []string{""},
|
||||
},
|
||||
}
|
||||
|
||||
if err := httpcfg[id].CheckForCli(); err != nil {
|
||||
return "", nil, xerrors.Errorf("check for cli: %w", err)
|
||||
}
|
||||
|
||||
svc, err := frpclient.NewService(cfg, httpcfg, nil, "")
|
||||
if err != nil {
|
||||
return "", nil, xerrors.Errorf("create new proxy service: %w", err)
|
||||
}
|
||||
|
||||
ch := make(chan error, 1)
|
||||
go func() {
|
||||
err := svc.Run()
|
||||
ch <- err
|
||||
close(ch)
|
||||
}()
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
svc.Close()
|
||||
}()
|
||||
|
||||
return fmt.Sprintf("https://%s.try.coder.app", subdomain), ch, nil
|
||||
}
|
@ -1,18 +1,18 @@
|
||||
package tunnel_test
|
||||
package devtunnel_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/tunnel"
|
||||
"github.com/coder/coder/coderd/devtunnel"
|
||||
)
|
||||
|
||||
// The tunnel leaks a few goroutines that aren't impactful to production scenarios.
|
||||
@ -22,9 +22,7 @@ import (
|
||||
|
||||
func TestTunnel(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() || os.Getenv("CI") != "" {
|
||||
// This test has extreme inconsistency in CI.
|
||||
// It's something with the networking in CI that causes this test to flake.
|
||||
if testing.Short() {
|
||||
t.Skip()
|
||||
return
|
||||
}
|
||||
@ -36,12 +34,16 @@ func TestTunnel(t *testing.T) {
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
defer cancelFunc()
|
||||
url, _, err := tunnel.New(ctx, srv.URL)
|
||||
|
||||
srvURL, err := url.Parse(srv.URL)
|
||||
require.NoError(t, err)
|
||||
t.Log(url)
|
||||
|
||||
tunURL, _, err := devtunnel.New(ctx, srvURL)
|
||||
require.NoError(t, err)
|
||||
t.Log(tunURL)
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", tunURL, nil)
|
||||
require.NoError(t, err)
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
var dnsErr *net.DNSError
|
@ -1,117 +0,0 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/retry"
|
||||
)
|
||||
|
||||
// New creates a new tunnel pointing at the URL provided.
|
||||
// Once created, it returns the external hostname that will resolve to it.
|
||||
//
|
||||
// The tunnel will exit when the context provided is canceled.
|
||||
//
|
||||
// Upstream connection occurs async through Cloudflare, so the error channel
|
||||
// will only be executed if the tunnel has failed after numerous attempts.
|
||||
func New(ctx context.Context, url string) (string, <-chan error, error) {
|
||||
_ = os.Setenv("QUIC_GO_DISABLE_RECEIVE_BUFFER_WARNING", "true")
|
||||
|
||||
httpTimeout := time.Second * 30
|
||||
client := http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSHandshakeTimeout: httpTimeout,
|
||||
ResponseHeaderTimeout: httpTimeout,
|
||||
},
|
||||
Timeout: httpTimeout,
|
||||
}
|
||||
|
||||
// Taken from:
|
||||
// https://github.com/cloudflare/cloudflared/blob/22cd8ceb8cf279afc1c412ae7f98308ffcfdd298/cmd/cloudflared/tunnel/quick_tunnel.go#L38
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.trycloudflare.com/tunnel", nil)
|
||||
if err != nil {
|
||||
return "", nil, xerrors.Errorf("create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", nil, xerrors.Errorf("request quick tunnel: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var data quickTunnelResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&data)
|
||||
if err != nil {
|
||||
return "", nil, xerrors.Errorf("decode: %w", err)
|
||||
}
|
||||
tunnelID, err := uuid.Parse(data.Result.ID)
|
||||
if err != nil {
|
||||
return "", nil, xerrors.Errorf("parse tunnel id: %w", err)
|
||||
}
|
||||
|
||||
credentials := connection.Credentials{
|
||||
AccountTag: data.Result.AccountTag,
|
||||
TunnelSecret: data.Result.Secret,
|
||||
TunnelID: tunnelID,
|
||||
}
|
||||
|
||||
namedTunnel := &connection.NamedTunnelProperties{
|
||||
Credentials: credentials,
|
||||
QuickTunnelUrl: data.Result.Hostname,
|
||||
}
|
||||
|
||||
set := flag.NewFlagSet("", 0)
|
||||
set.String("protocol", "", "")
|
||||
set.String("url", "", "")
|
||||
// set.Int("retries", 5, "")
|
||||
appCtx := cli.NewContext(&cli.App{}, set, nil)
|
||||
appCtx.Context = ctx
|
||||
_ = appCtx.Set("url", url)
|
||||
_ = appCtx.Set("protocol", "quic")
|
||||
logger := zerolog.New(os.Stdout).Level(zerolog.Disabled)
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
for retry.New(250*time.Millisecond, 5*time.Second).Wait(ctx) {
|
||||
err := tunnel.StartServer(appCtx, &cliutil.BuildInfo{}, namedTunnel, &logger, false)
|
||||
if err != nil && strings.Contains(err.Error(), "Failed to get tunnel") {
|
||||
continue
|
||||
}
|
||||
errCh <- err
|
||||
}
|
||||
}()
|
||||
if !strings.HasPrefix(data.Result.Hostname, "https://") {
|
||||
data.Result.Hostname = "https://" + data.Result.Hostname
|
||||
}
|
||||
return data.Result.Hostname, errCh, nil
|
||||
}
|
||||
|
||||
type quickTunnelResponse struct {
|
||||
Success bool
|
||||
Result quickTunnel
|
||||
Errors []quickTunnelError
|
||||
}
|
||||
|
||||
type quickTunnelError struct {
|
||||
Code int
|
||||
Message string
|
||||
}
|
||||
|
||||
type quickTunnel struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Hostname string `json:"hostname"`
|
||||
AccountTag string `json:"account_tag"`
|
||||
Secret []byte `json:"secret"`
|
||||
}
|
Reference in New Issue
Block a user