Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .drone.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ local bootstrap = '25.02';
local nginx = '1.24.0';
local python = '3.12-slim-bookworm';
local alpine = '3.21';
local visual_diff_skip_build = '3040';
local visual_diff_skip_build = '3055';

local build(arch, testUI) = [{
kind: 'pipeline',
Expand Down Expand Up @@ -335,7 +335,7 @@ local build(arch, testUI) = [{
name: 'relay.redirect',
image: 'snowdreamtech/frps:0.61.1',
commands: [
'printf "bindPort = 443\nvhostHTTPSPort = 4443\n" > /etc/frp/frps.toml',
'printf "bindPort = 443\nvhostHTTPSPort = 4443\ntcpMuxHTTPConnectPort = 1337\n" > /etc/frp/frps.toml',
'/usr/bin/frps -c /etc/frp/frps.toml',
],
},
Expand Down
19 changes: 8 additions & 11 deletions backend/access/external_address.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ type Redirect interface {
}

type Relay interface {
Enable() error
Disable() error
Apply(relayEnabled bool) error
}

type Trigger interface {
Expand Down Expand Up @@ -78,14 +77,8 @@ func (a *ExternalAddress) Update(request model.Access) error {
a.logger.Info(fmt.Sprintf("update relay: %v, ipv4 enabled: %v, ipv4 public: %v, ipv6 enabled: %v",
request.RelayEnabled, request.Ipv4Enabled, request.Ipv4Public, request.Ipv6Enabled))

if request.RelayEnabled {
if err := a.relay.Enable(); err != nil {
return err
}
} else {
if err := a.relay.Disable(); err != nil {
return err
}
if err := a.relay.Apply(request.RelayEnabled); err != nil {
return err
}

ipv4 := request.Ipv4
Expand Down Expand Up @@ -152,6 +145,10 @@ func (a *ExternalAddress) Update(request model.Access) error {
a.userConfig.SetIpv6Enabled(request.Ipv6Enabled)
a.userConfig.SetPublicPort(request.AccessPort)

if err := a.relay.Apply(request.RelayEnabled); err != nil {
return err
}

a.trigger.Trigger()
return nil

Expand All @@ -171,5 +168,5 @@ func (a *ExternalAddress) Sync() error {
return err
}
}
return nil
return a.relay.Apply(a.userConfig.IsRelayEnabled())
}
52 changes: 35 additions & 17 deletions backend/access/external_address_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,16 @@ type RedirectStub struct {
type RelayStub struct {
enabled bool
disabled bool
applied int
}

func (r *RelayStub) Enable() error {
r.enabled = true
return nil
}

func (r *RelayStub) Disable() error {
r.disabled = true
func (r *RelayStub) Apply(relayEnabled bool) error {
r.applied++
if relayEnabled {
r.enabled = true
} else {
r.disabled = true
}
return nil
}

Expand Down Expand Up @@ -98,23 +99,19 @@ func (u *ExternalAddressUserConfigStub) SetPublicPort(port *int) {
}

func (u *ExternalAddressUserConfigStub) GetPublicIp() *string {
//TODO implement me
panic("implement me")
return u.publicIp
}

func (u *ExternalAddressUserConfigStub) GetPublicPort() *int {
//TODO implement me
panic("implement me")
return nil
}

func (u *ExternalAddressUserConfigStub) IsIpv6Enabled() bool {
//TODO implement me
panic("implement me")
return false
}

func (u *ExternalAddressUserConfigStub) IsIpv4Public() bool {
//TODO implement me
panic("implement me")
return false
}

func (u *ExternalAddressUserConfigStub) IsRelayEnabled() bool {
Expand All @@ -125,8 +122,7 @@ func (u *ExternalAddressUserConfigStub) SetRelayEnabled(enabled bool) {
}

func (u *ExternalAddressUserConfigStub) IsIpv4Enabled() bool {
//TODO implement me
panic("implement me")
return true
}

func TestExternalAddress_UpdateWithIpv4(t *testing.T) {
Expand Down Expand Up @@ -224,3 +220,25 @@ func TestExternalAddress_Ipv4Private_NoProbe(t *testing.T) {
assert.Nil(t, config.publicIp)
assert.Equal(t, 0, len(probe.probed))
}

func TestExternalAddress_UpdateAppliesTheTunnelAgainAfterTheAddressUpdate(t *testing.T) {
relay := &RelayStub{}
access := New(NewPoptProbeStub(), &ExternalAddressUserConfigStub{}, &RedirectStub{}, relay,
&TriggerStub{}, &NetworkInfoStub{publicIPv4: "2.2.2.2"}, log.Default())

err := access.Update(model.Access{Ipv4Enabled: true, Ipv4Public: false})

assert.Nil(t, err)
assert.Equal(t, 2, relay.applied)
}

func TestExternalAddress_SyncAppliesTheTunnel(t *testing.T) {
relay := &RelayStub{}
access := New(NewPoptProbeStub(), &ExternalAddressUserConfigStub{}, &RedirectStub{}, relay,
&TriggerStub{}, &NetworkInfoStub{publicIPv4: "2.2.2.2"}, log.Default())

err := access.Sync()

assert.Nil(t, err)
assert.Equal(t, 1, relay.applied)
}
88 changes: 65 additions & 23 deletions backend/access/relay_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const (
relayAdminSocket = "frpc-admin.sock"
relayAdminUrl = "http://unix/api/status"
relayConnectAttempts = 30

SmtpProxySuffix = "-smtp"
)

type RelayControl interface {
Expand All @@ -38,11 +40,13 @@ type frpcConfig struct {
AdminSocket string
Domain string
LocalPort int
MailSocket string
}

type RelayUserConfig interface {
GetDeviceDomain() string
GetDomainUpdateToken() *string
GetMailInboundSocket() *string
}

type RelayRedirectConfig interface {
Expand All @@ -56,6 +60,8 @@ type RelayClient struct {
redirect RelayRedirectConfig
client *http.Client
logger *zap.Logger

connectAttempts int
}

func NewRelayClient(control RelayControl, systemConfig RelaySystemConfig, userConfig RelayUserConfig, redirect RelayRedirectConfig, client *http.Client, logger *zap.Logger) *RelayClient {
Expand All @@ -66,6 +72,8 @@ func NewRelayClient(control RelayControl, systemConfig RelaySystemConfig, userCo
redirect: redirect,
client: client,
logger: logger,

connectAttempts: relayConnectAttempts,
}
}

Expand All @@ -89,7 +97,12 @@ func (c *RelayClient) adminSocketPath() string {
return filepath.Join(c.systemConfig.DataDir(), relayAdminSocket)
}

func (c *RelayClient) Enable() error {
func (c *RelayClient) Apply(relayEnabled bool) error {
if !relayEnabled {
return c.Disable()
}
mailSocket := c.userConfig.GetMailInboundSocket()

domain := c.userConfig.GetDeviceDomain()
token := c.userConfig.GetDomainUpdateToken()
if token == nil {
Expand All @@ -100,31 +113,49 @@ func (c *RelayClient) Enable() error {
if err != nil {
return err
}
var content bytes.Buffer
err = tmpl.Execute(&content, frpcConfig{
settings := frpcConfig{
Server: server,
Token: *token,
AdminSocket: c.adminSocketPath(),
Domain: domain,
LocalPort: config.WebAccessPort,
})
if err != nil {
MailSocket: socketPath(mailSocket),
}
var content bytes.Buffer
if err := tmpl.Execute(&content, settings); err != nil {
return err
}

if c.currentConfig() == content.String() && c.proxyRunning(domain) {
c.logger.Info("relay already connected, skipping restart", zap.String("domain", domain))
expected := c.expectedProxies(domain, mailSocket != nil)
if c.currentConfig() == content.String() && c.proxiesRunning(expected) {
c.logger.Info("relay already connected, skipping restart", zap.Strings("proxies", expected))
return nil
}

c.logger.Info("enabling relay", zap.String("server", server), zap.String("domain", domain))
c.logger.Info("applying relay",
zap.String("server", server), zap.Strings("proxies", expected))
if err := os.WriteFile(c.configPath(), content.Bytes(), 0644); err != nil {
return err
}
if err := c.control.RestartService(RelayService); err != nil {
return err
}
return c.waitConnected(domain)
return c.waitConnected(expected)
}

func (c *RelayClient) expectedProxies(domain string, mail bool) []string {
proxies := []string{domain}
if mail {
proxies = append(proxies, domain+SmtpProxySuffix)
}
return proxies
}

func socketPath(socket *string) string {
if socket == nil {
return ""
}
return *socket
}

func (c *RelayClient) currentConfig() string {
Expand All @@ -135,38 +166,49 @@ func (c *RelayClient) currentConfig() string {
return string(content)
}

func (c *RelayClient) waitConnected(domain string) error {
for attempt := 0; attempt < relayConnectAttempts; attempt++ {
if c.proxyRunning(domain) {
c.logger.Info("relay tunnel connected", zap.String("domain", domain))
func (c *RelayClient) waitConnected(proxies []string) error {
for attempt := 0; attempt < c.connectAttempts; attempt++ {
if c.proxiesRunning(proxies) {
c.logger.Info("relay tunnel connected", zap.Strings("proxies", proxies))
return nil
}
time.Sleep(time.Second)
}
return fmt.Errorf("relay tunnel did not come up for %s", domain)
return fmt.Errorf("relay tunnel did not come up for %v", proxies)
}

func (c *RelayClient) proxiesRunning(proxies []string) bool {
running := c.runningProxies()
for _, name := range proxies {
if !running[name] {
return false
}
}
return true
}

func (c *RelayClient) proxyRunning(domain string) bool {
func (c *RelayClient) runningProxies() map[string]bool {
result := map[string]bool{}
resp, err := c.client.Get(relayAdminUrl)
if err != nil {
return false
return result
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return false
return result
}
var status map[string][]relayProxyStatus
if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
return false
return result
}
for _, proxies := range status {
for _, p := range proxies {
if p.Name == domain && p.Status == "running" {
return true
for _, group := range status {
for _, p := range group {
if p.Status == "running" {
result[p.Name] = true
}
}
}
return false
return result
}

func (c *RelayClient) Disable() error {
Expand Down
Loading