Agent posts an empty beacon over a tcp4-forced connection every 5 minutes (plus once at startup); panel files the observed source as SeenIPv4 only when it is a public IPv4 literal, never touching the reported telemetry address, so 1:1 NAT nodes stop falling back to their private address in share links.
59 lines
2.1 KiB
Go
59 lines
2.1 KiB
Go
package agent
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// Beacon cadence is deliberately slow: the proven address barely changes, so the heartbeat's 60 s would be pure noise; the first beacon fires at startup.
|
|
const defaultV4BeaconInterval = 5 * time.Minute
|
|
|
|
// v4OnlyDialContext forces every dial onto tcp4: on 1:1 NAT the local v4 is private, so only a panel-observed v4 arrival proves the public address (a v6-only panel just fails the dial).
|
|
func v4OnlyDialContext(dial func(ctx context.Context, network, address string) (net.Conn, error)) func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
return func(ctx context.Context, _ string, address string) (net.Conn, error) {
|
|
return dial(ctx, "tcp4", address)
|
|
}
|
|
}
|
|
|
|
// newV4BeaconClient mirrors the agent's normal client (same TLS trust, no proxy — a proxy's egress would prove nothing about this host) except its dialer is pinned to tcp4.
|
|
func newV4BeaconClient() *http.Client {
|
|
return &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
|
|
DialContext: v4OnlyDialContext((&net.Dialer{Timeout: 10 * time.Second}).DialContext),
|
|
},
|
|
}
|
|
}
|
|
|
|
// sendV4Beacon posts one empty beacon over a v4-only connection; the panel's observed source is the whole signal, since any self-reported address is worthless behind 1:1 NAT. Any failure is the caller's to ignore — the heartbeat keeps working over v6 meanwhile.
|
|
func (a *Agent) sendV4Beacon() error {
|
|
url := fmt.Sprintf("%s/panel/api/agent/v4-beacon", a.config.MasterURL)
|
|
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader([]byte(`{}`)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+a.config.Token)
|
|
client := a.v4httpClient
|
|
if client == nil {
|
|
client = newV4BeaconClient()
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
_, _ = io.Copy(io.Discard, resp.Body)
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("v4 beacon failed with status %d", resp.StatusCode)
|
|
}
|
|
return nil
|
|
}
|