mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
[优化] 增加对节点 IP 的自动探测,优先通过第三方 API 获取公网 IP
This commit is contained in:
@@ -192,4 +192,4 @@ curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/unin
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=Rain-kl/OpenFlare&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=Rain-kl/OpenFlare&type=date&legend=top-left" />
|
||||
</picture>
|
||||
</a>
|
||||
</a>
|
||||
|
||||
@@ -61,7 +61,7 @@ Agent supports the `-config` CLI flag, an `agent.json` file, and the `LOG_LEVEL`
|
||||
| `agent_token` | Node-specific auth token | one of `agent_token` / `discovery_token` | empty |
|
||||
| `discovery_token` | Global token for first registration | one of `agent_token` / `discovery_token` | empty |
|
||||
| `node_name` | Node name | no | host name |
|
||||
| `node_ip` | Node IP | no | auto-detected |
|
||||
| `node_ip` | Node IP | no | auto-detected; Agent first queries the public egress IP through a third-party API, then falls back to local interfaces |
|
||||
| `openresty_path` | OpenResty binary path | no | `openresty` |
|
||||
| `openresty_container_name` | Deprecated Docker-control field, read for compatibility only | no | empty |
|
||||
| `openresty_docker_image` | Deprecated Docker-control field, read for compatibility only | no | empty |
|
||||
@@ -74,3 +74,5 @@ Agent supports the `-config` CLI flag, an `agent.json` file, and the `LOG_LEVEL`
|
||||
| `request_timeout` | HTTP timeout | no | `10000` ms |
|
||||
|
||||
`heartbeat_interval` and `request_timeout` accept milliseconds or Go duration strings.
|
||||
|
||||
When `node_ip` is not configured, Agent first queries `https://realip.cc` for the real public egress IP, which avoids recording a Docker bridge address in container deployments. If that lookup fails, Agent falls back to local interface detection and prefers a public IPv4 address.
|
||||
|
||||
@@ -167,7 +167,7 @@ journalctl -u openflare-agent -f
|
||||
|
||||
## Docker 运行 Agent
|
||||
|
||||
Docker 部署时直接运行 Agent 镜像。该镜像基于 OpenResty 镜像制作,内置 Agent 控制器与 OpenResty 二进制。
|
||||
Docker 部署时直接运行 Agent 镜像。该镜像基于 OpenResty 镜像制作,内置 Agent 控制器与 OpenResty 二进制。未显式配置 `node_ip` 时,Agent 会优先通过第三方 API 获取真实出口 IP,避免把 Docker 网桥地址登记为节点 IP。
|
||||
|
||||
挂载配置文件:
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ OpenResty 性能参数与缓存参数继续统一保存在 `Option` 表。当前
|
||||
| `agent_token` | 节点专属认证 Token | 与 `discovery_token` 二选一 | 空 |
|
||||
| `discovery_token` | 首次自动注册使用的全局 Token | 与 `agent_token` 二选一 | 空 |
|
||||
| `node_name` | 节点名称 | 否 | 自动使用主机名 |
|
||||
| `node_ip` | 节点 IP | 否 | 自动探测,优先选择公网 IPv4;仅无公网地址时退回可用内网地址 |
|
||||
| `node_ip` | 节点 IP | 否 | 自动探测,优先通过第三方 API 获取真实出口公网 IP;失败时退回本机网卡探测 |
|
||||
| `openresty_path` | OpenResty 二进制路径 | 否 | `openresty` |
|
||||
| `openresty_container_name` | 旧 Docker 控制字段,仅兼容读取 | 否 | 空 |
|
||||
| `openresty_docker_image` | 旧 Docker 控制字段,仅兼容读取 | 否 | 空 |
|
||||
@@ -192,6 +192,7 @@ OpenResty 性能参数与缓存参数继续统一保存在 `Option` 表。当前
|
||||
* 未配置 `openresty_path` 时默认调用 `openresty`。
|
||||
* Agent 周期性健康检查会请求 `http://127.0.0.1:<openresty_observability_port>/openflare/stub_status`,不再通过高频 `openresty -t` 判断运行时健康;配置应用、启动恢复和 reload 前校验仍会执行 `openresty -t -c <main_config_path>`。
|
||||
* 如果 `agent.json` 不存在,但 `OPENFLARE_SERVER_URL` 与 Token 等环境变量足够,Agent 可以直接启动;两者同时存在时环境变量优先。
|
||||
* Agent 未配置 `node_ip` 时,会优先通过 `https://realip.cc` 获取真实出口公网 IP,适配 Docker/NAT 场景;该请求失败时,才退回本机网卡探测并优先选择公网 IPv4。
|
||||
* Agent 自动探测到私网 `node_ip` 时,Server 会在注册/心跳阶段优先保留 Agent 直连来源的公网地址,避免 NAT/多网卡场景误登记内网网卡地址。
|
||||
|
||||
## 常见配置组合
|
||||
|
||||
@@ -7,4 +7,12 @@ require (
|
||||
openflare v0.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
)
|
||||
|
||||
replace openflare => ../openflare_server
|
||||
|
||||
@@ -1,2 +1,22 @@
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM=
|
||||
github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"openflare/utils/geoip"
|
||||
"openflare/utils/geoip/iputil"
|
||||
"os"
|
||||
pathpkg "path"
|
||||
@@ -27,6 +29,11 @@ const (
|
||||
defaultObservabilityReplayMinutes = 15
|
||||
)
|
||||
|
||||
var (
|
||||
lookupOutboundIP = geoip.GetOutboundIP
|
||||
lookupLocalIP = detectLocalNodeIP
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ServerURL string `json:"server_url"`
|
||||
AgentToken string `json:"agent_token"`
|
||||
@@ -403,6 +410,23 @@ func firstNonEmpty(values ...string) string {
|
||||
}
|
||||
|
||||
func detectNodeIP() string {
|
||||
if ip := detectOutboundNodeIP(); ip != "" {
|
||||
return ip
|
||||
}
|
||||
return lookupLocalIP()
|
||||
}
|
||||
|
||||
func detectOutboundNodeIP() string {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
ip, err := lookupOutboundIP(ctx)
|
||||
if err != nil || ip == nil {
|
||||
return ""
|
||||
}
|
||||
return ip.String()
|
||||
}
|
||||
|
||||
func detectLocalNodeIP() string {
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return ""
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"openflare/utils/geoip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -278,6 +281,77 @@ func TestLoadUsesEnvConfigWhenFileIsMissing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDetectsOutboundIPWhenNodeIPMissing(t *testing.T) {
|
||||
previousLookup := lookupOutboundIP
|
||||
lookupOutboundIP = func(ctx context.Context, strategies ...geoip.OutboundIPStrategy) (net.IP, error) {
|
||||
return net.ParseIP("8.8.8.8"), nil
|
||||
}
|
||||
defer func() {
|
||||
lookupOutboundIP = previousLookup
|
||||
}()
|
||||
|
||||
dir := t.TempDir()
|
||||
configPath := filepath.Join(dir, "agent.json")
|
||||
payload := map[string]any{
|
||||
"server_url": "http://127.0.0.1:3000",
|
||||
"agent_token": "token",
|
||||
"node_name": "edge-01",
|
||||
}
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal config: %v", err)
|
||||
}
|
||||
if err = os.WriteFile(configPath, data, 0o644); err != nil {
|
||||
t.Fatalf("failed to write config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := Load(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load failed: %v", err)
|
||||
}
|
||||
if cfg.NodeIP != "8.8.8.8" {
|
||||
t.Fatalf("expected outbound IP, got %s", cfg.NodeIP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFallsBackToLocalIPWhenOutboundLookupFails(t *testing.T) {
|
||||
previousOutboundLookup := lookupOutboundIP
|
||||
previousLocalLookup := lookupLocalIP
|
||||
lookupOutboundIP = func(ctx context.Context, strategies ...geoip.OutboundIPStrategy) (net.IP, error) {
|
||||
return nil, errors.New("realip.cc unavailable")
|
||||
}
|
||||
lookupLocalIP = func() string {
|
||||
return "9.9.9.9"
|
||||
}
|
||||
defer func() {
|
||||
lookupOutboundIP = previousOutboundLookup
|
||||
lookupLocalIP = previousLocalLookup
|
||||
}()
|
||||
|
||||
dir := t.TempDir()
|
||||
configPath := filepath.Join(dir, "agent.json")
|
||||
payload := map[string]any{
|
||||
"server_url": "http://127.0.0.1:3000",
|
||||
"agent_token": "token",
|
||||
"node_name": "edge-01",
|
||||
}
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal config: %v", err)
|
||||
}
|
||||
if err = os.WriteFile(configPath, data, 0o644); err != nil {
|
||||
t.Fatalf("failed to write config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := Load(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load failed: %v", err)
|
||||
}
|
||||
if cfg.NodeIP != "9.9.9.9" {
|
||||
t.Fatalf("expected local fallback IP, got %s", cfg.NodeIP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadEnvOverridesConfigFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
configPath := filepath.Join(dir, "agent.json")
|
||||
|
||||
@@ -69,7 +69,7 @@ func main() {
|
||||
|
||||
// Initialize options
|
||||
model.InitOptionMap()
|
||||
geoip.InitGeoIP()
|
||||
geoip.InitGeoIP(common.GeoIPProvider)
|
||||
backgroundCtx, cancelBackgroundTasks := context.WithCancel(context.Background())
|
||||
defer cancelBackgroundTasks()
|
||||
service.StartDatabaseAutoCleanupScheduler(backgroundCtx)
|
||||
|
||||
@@ -406,6 +406,6 @@ func updateOptionMap(key string, value string) {
|
||||
}
|
||||
common.OptionMapRWMutex.Unlock()
|
||||
if shouldRefreshGeoIP {
|
||||
geoip.InitGeoIP()
|
||||
geoip.InitGeoIP(common.GeoIPProvider)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"openflare/common"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -110,8 +109,8 @@ func GetRegionUnicodeEmoji(isoCode string) string {
|
||||
return string(rune1) + string(rune2)
|
||||
}
|
||||
|
||||
func InitGeoIP() {
|
||||
providerName := normalizeProvider(common.GeoIPProvider)
|
||||
func InitGeoIP(provider string) {
|
||||
providerName := normalizeProvider(provider)
|
||||
nextProvider, err := providerFactory(providerName)
|
||||
if err != nil {
|
||||
slog.Error("initialize GeoIP provider failed", "provider", providerName, "error", err)
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"openflare/utils/geoip/iputil"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultOutboundIPLookupTimeout = 5 * time.Second
|
||||
|
||||
// OutboundIPStrategy defines a lookup strategy for the current public egress IP.
|
||||
type OutboundIPStrategy interface {
|
||||
Name() string
|
||||
GetOutboundIP(ctx context.Context) (net.IP, error)
|
||||
}
|
||||
|
||||
// OutboundIPAPIAdapter adapts a third-party HTTP API response into an IP value.
|
||||
type OutboundIPAPIAdapter interface {
|
||||
Name() string
|
||||
Endpoint() string
|
||||
DecodeIP(io.Reader) (net.IP, error)
|
||||
}
|
||||
|
||||
type HTTPOutboundIPStrategy struct {
|
||||
Client *http.Client
|
||||
Adapter OutboundIPAPIAdapter
|
||||
}
|
||||
|
||||
func NewHTTPOutboundIPStrategy(adapter OutboundIPAPIAdapter, client *http.Client) *HTTPOutboundIPStrategy {
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: defaultOutboundIPLookupTimeout}
|
||||
}
|
||||
return &HTTPOutboundIPStrategy{
|
||||
Client: client,
|
||||
Adapter: adapter,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HTTPOutboundIPStrategy) Name() string {
|
||||
if s == nil || s.Adapter == nil {
|
||||
return "http-outbound-ip"
|
||||
}
|
||||
return s.Adapter.Name()
|
||||
}
|
||||
|
||||
func (s *HTTPOutboundIPStrategy) GetOutboundIP(ctx context.Context) (net.IP, error) {
|
||||
if s == nil || s.Adapter == nil {
|
||||
return nil, errors.New("outbound IP adapter is nil")
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
client := s.Client
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: defaultOutboundIPLookupTimeout}
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, s.Adapter.Endpoint(), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s create request failed: %w", s.Name(), err)
|
||||
}
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s request failed: %w", s.Name(), err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%s returned non-200 status: %d %s", s.Name(), response.StatusCode, response.Status)
|
||||
}
|
||||
ip, err := s.Adapter.DecodeIP(response.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s decode response failed: %w", s.Name(), err)
|
||||
}
|
||||
if !iputil.IsPublic(ip) {
|
||||
return nil, fmt.Errorf("%s returned non-public IP: %s", s.Name(), ip.String())
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
type RealIPCCAdapter struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
type realIPCCResponse struct {
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
func NewRealIPCCOutboundIPStrategy() *HTTPOutboundIPStrategy {
|
||||
return NewHTTPOutboundIPStrategy(RealIPCCAdapter{}, nil)
|
||||
}
|
||||
|
||||
func (a RealIPCCAdapter) Name() string {
|
||||
return "realip.cc"
|
||||
}
|
||||
|
||||
func (a RealIPCCAdapter) Endpoint() string {
|
||||
if strings.TrimSpace(a.URL) != "" {
|
||||
return strings.TrimSpace(a.URL)
|
||||
}
|
||||
return "https://realip.cc"
|
||||
}
|
||||
|
||||
func (a RealIPCCAdapter) DecodeIP(reader io.Reader) (net.IP, error) {
|
||||
var payload realIPCCResponse
|
||||
if err := json.NewDecoder(reader).Decode(&payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ip := net.ParseIP(strings.TrimSpace(payload.IP))
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("invalid IP %q", payload.IP)
|
||||
}
|
||||
if ipv4 := ip.To4(); ipv4 != nil {
|
||||
return ipv4, nil
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func DefaultOutboundIPStrategies() []OutboundIPStrategy {
|
||||
return []OutboundIPStrategy{
|
||||
NewRealIPCCOutboundIPStrategy(),
|
||||
}
|
||||
}
|
||||
|
||||
func GetOutboundIP(ctx context.Context, strategies ...OutboundIPStrategy) (net.IP, error) {
|
||||
if len(strategies) == 0 {
|
||||
strategies = DefaultOutboundIPStrategies()
|
||||
}
|
||||
var errs []error
|
||||
for _, strategy := range strategies {
|
||||
if strategy == nil {
|
||||
continue
|
||||
}
|
||||
ip, err := strategy.GetOutboundIP(ctx)
|
||||
if err == nil && ip != nil {
|
||||
return ip, nil
|
||||
}
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("%s: %w", strategy.Name(), err))
|
||||
}
|
||||
}
|
||||
if len(errs) == 0 {
|
||||
return nil, errors.New("no outbound IP lookup strategy configured")
|
||||
}
|
||||
return nil, errors.Join(errs...)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeOutboundIPStrategy struct {
|
||||
name string
|
||||
ip net.IP
|
||||
err error
|
||||
}
|
||||
|
||||
func (f fakeOutboundIPStrategy) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (f fakeOutboundIPStrategy) GetOutboundIP(ctx context.Context) (net.IP, error) {
|
||||
return f.ip, f.err
|
||||
}
|
||||
|
||||
func TestRealIPCCAdapterDecodeIP(t *testing.T) {
|
||||
ip, err := RealIPCCAdapter{}.DecodeIP(strings.NewReader(`{"ip":"8.8.8.8","country":"United States"}`))
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeIP failed: %v", err)
|
||||
}
|
||||
if ip.String() != "8.8.8.8" {
|
||||
t.Fatalf("unexpected IP: %s", ip.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPOutboundIPStrategyUsesAdapter(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Fatalf("unexpected method: %s", r.Method)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"ip":"8.8.4.4"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
strategy := NewHTTPOutboundIPStrategy(RealIPCCAdapter{URL: server.URL}, server.Client())
|
||||
ip, err := strategy.GetOutboundIP(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("GetOutboundIP failed: %v", err)
|
||||
}
|
||||
if ip.String() != "8.8.4.4" {
|
||||
t.Fatalf("unexpected outbound IP: %s", ip.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOutboundIPFallsBackToNextStrategy(t *testing.T) {
|
||||
ip, err := GetOutboundIP(
|
||||
context.Background(),
|
||||
fakeOutboundIPStrategy{name: "first", err: errors.New("temporary failure")},
|
||||
fakeOutboundIPStrategy{name: "second", ip: net.ParseIP("1.1.1.1")},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("GetOutboundIP failed: %v", err)
|
||||
}
|
||||
if ip.String() != "1.1.1.1" {
|
||||
t.Fatalf("unexpected outbound IP: %s", ip.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPOutboundIPStrategyRejectsPrivateIP(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"ip":"172.17.0.2"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
strategy := NewHTTPOutboundIPStrategy(RealIPCCAdapter{URL: server.URL}, server.Client())
|
||||
if _, err := strategy.GetOutboundIP(context.Background()); err == nil {
|
||||
t.Fatal("expected private IP to be rejected")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user