Skip to content

Commit 5e834fc

Browse files
ignoramousCopilot
andcommitted
ipn: use Router.Self to break ProxyTo loops
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 254d3fb commit 5e834fc

16 files changed

Lines changed: 230 additions & 22 deletions

File tree

intra/backend/ipn_proxies.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ type Router interface {
357357
Reaches(hostportOrIPPortCsv string) (y bool)
358358
// Contains returns true if this router can route ipprefix.
359359
Contains(ipprefix string) (y bool)
360+
// Self returns true if the given ip is one of this router's own exit IPs.
361+
Self(ip string) (y bool)
360362
}
361363

362364
type Client interface {

intra/dialers/ips.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ func NewProtected(hostOrIP string, ipps []string) (*ipmap.IPSet, bool) {
8383
// For returns addresses for hostOrIP from cache, resolving them if missing.
8484
// Underlying cache relies on Disconfirm() to remove unreachable IP addrs;
8585
// if not called, these entries may go stale. Use Resolve() to bypass cache.
86+
// Use CachedAddrs() to only ever return from cache.
8687
// hostOrIP may be host:port, or ip:port, or host, or ip.
8788
func For(hostOrIP string) []netip.Addr {
8889
ipset := ipm.Get(hostOrIP)

intra/ipn/auto.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,30 @@ func (h *auto) Reaches(hostportOrIPPortCsv string) bool {
463463
return Reaches(h, hostportOrIPPortCsv)
464464
}
465465

466+
// Self implements x.Router.
467+
func (h *auto) Self(ip string) bool {
468+
if len(ip) <= 0 {
469+
return false
470+
}
471+
if settings.AutoAlwaysRemote() {
472+
if win, _ := h.pxr.mainRpnProxyOf(RpnWin); win != nil {
473+
if iscircular(win, ip) {
474+
return true
475+
}
476+
}
477+
if exit64, _ := h.pxr.ProxyFor(Rpn64); exit64 != nil {
478+
if iscircular(exit64, ip) {
479+
return true
480+
}
481+
}
482+
return false
483+
}
484+
if exit, _ := h.pxr.ProxyFor(Exit); exit != nil {
485+
return iscircular(exit, ip)
486+
}
487+
return false
488+
}
489+
466490
// Hop implements Proxy.
467491
func (h *auto) Hop(via *core.WeakRef[Proxy], dryrun bool) error {
468492
var winerr error

intra/ipn/base.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ type base struct {
3636
via atomic.Pointer[core.WeakRef[Proxy]] // via dialer
3737
px ProxyProvider
3838
status atomic.Int32
39-
lastaddr atomic.Pointer[string]
4039
done context.CancelFunc
4140
}
4241

intra/ipn/exit.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ type exit struct {
4141
dhdl uint64
4242
outbound *protect.RDial // outbound dialer
4343
status atomic.Int32
44-
lastaddr atomic.Pointer[string]
4544
done context.CancelFunc
4645
}
4746

intra/ipn/exit64.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ type exit64 struct {
4343
hdl uint64
4444
dhdl uint64
4545
status atomic.Int32
46-
lastaddr atomic.Pointer[string]
4746
done context.CancelFunc
4847
}
4948

intra/ipn/http1.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ type http1 struct {
3737
opts *settings.ProxyOptions
3838
lastdial atomic.Int64
3939
status atomic.Int32
40-
lastaddr atomic.Pointer[string]
4140
}
4241

4342
func NewHTTPProxy(id string, ctx context.Context, c protect.Controller, px ProxyProvider, po *settings.ProxyOptions) (*http1, error) {
@@ -161,6 +160,21 @@ func (h *http1) Reaches(hostportOrIPPortCsv string) bool {
161160
return Reaches(h, hostportOrIPPortCsv)
162161
}
163162

163+
// Self implements x.Router.
164+
func (h *http1) Self(ip string) bool {
165+
if len(ip) <= 0 {
166+
return false
167+
}
168+
// Check cached IPs of the upstream proxy host
169+
for _, a := range dialers.CachedAddrs(h.opts.Host) {
170+
if a.String() == ip {
171+
return true
172+
}
173+
}
174+
// fallback to last known dialed address
175+
return h.GW.Self(ip)
176+
}
177+
164178
// Hop implements Proxy.
165179
func (h *http1) Hop(via *core.WeakRef[Proxy], dryrun bool) error {
166180
if h.id == GlobalH1 {

intra/ipn/multihost/map.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type MHMap struct {
2424
uniq map[*MH]struct{}
2525
byIpp map[netip.AddrPort]*MH // ip:port => MH
2626
byHostport map[string]*MH // host:port => MH
27+
byAddr map[netip.Addr]int // addr => refcount; for O(1) HasAddr
2728
}
2829

2930
func (m *MHMap) All() (all []*MH) {
@@ -57,6 +58,20 @@ func (m *MHMap) Endpoints() (all []string) {
5758
return
5859
}
5960

61+
// HasAddr returns true if any endpoint in this map contains the given address.
62+
// It looks up byIpp directly for efficiency rather than iterating through all MHs.
63+
// HasAddr returns true if any endpoint in this map contains the given address.
64+
// Uses the byAddr index for O(1) lookup.
65+
func (m *MHMap) HasAddr(addr netip.Addr) bool {
66+
if m == nil || !addr.IsValid() {
67+
return false
68+
}
69+
m.RLock()
70+
defer m.RUnlock()
71+
_, ok := m.byAddr[addr]
72+
return ok
73+
}
74+
6075
func (m *MHMap) Get(hostOrIpport string) (h *MH, _ error) {
6176
if m == nil {
6277
return nil, errMhNotFound
@@ -116,6 +131,8 @@ func (m *MHMap) putLocked(h *MH) (ok bool) {
116131
m.uniq[h] = struct{}{}
117132
for _, ipp := range ipps {
118133
m.byIpp[ipp] = h
134+
// increment refcount for each unique addr (ignore port)
135+
m.byAddr[ipp.Addr()]++
119136
}
120137
for _, name := range names {
121138
m.byHostport[name] = h
@@ -149,6 +166,13 @@ func (m *MHMap) delLocked(h *MH) (ok bool) {
149166
for _, ip := range ipps {
150167
if x := m.byIpp[ip]; x == h {
151168
delete(m.byIpp, ip)
169+
// decrement refcount for each unique addr (ignore port)
170+
a := ip.Addr()
171+
if m.byAddr[a] <= 1 {
172+
delete(m.byAddr, a)
173+
} else {
174+
m.byAddr[a]--
175+
}
152176
}
153177
}
154178
for _, name := range names {
@@ -263,5 +287,6 @@ func NewMap(id string) *MHMap {
263287
uniq: make(map[*MH]struct{}),
264288
byIpp: make(map[netip.AddrPort]*MH),
265289
byHostport: make(map[string]*MH),
290+
byAddr: make(map[netip.Addr]int),
266291
}
267292
}

intra/ipn/multihost/multihost.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,38 @@ func (h *MH) Addrs() []netip.AddrPort {
124124
return slices.Concat(h.addrs, h.preresolved)
125125
}
126126

127+
// Has returns true if the given ip matches any address in this multihost.
128+
func (h *MH) Has(ip string) bool {
129+
if h == nil {
130+
return false
131+
}
132+
addr, err := netip.ParseAddr(ip)
133+
if err != nil {
134+
return false
135+
}
136+
return h.HasAddr(addr)
137+
}
138+
139+
// HasAddr returns true if the given addr matches any address in this multihost.
140+
func (h *MH) HasAddr(addr netip.Addr) bool {
141+
if h == nil || !addr.IsValid() {
142+
return false
143+
}
144+
h.RLock()
145+
defer h.RUnlock()
146+
for _, a := range h.addrs {
147+
if a.Addr() == addr {
148+
return true
149+
}
150+
}
151+
for _, a := range h.preresolved {
152+
if a.Addr() == addr {
153+
return true
154+
}
155+
}
156+
return false
157+
}
158+
127159
func (h *MH) splitFamily() (out4, out6, og []netip.AddrPort) {
128160
out4 = make([]netip.AddrPort, 0)
129161
out6 = make([]netip.AddrPort, 0)

intra/ipn/nop.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package ipn
88

99
import (
1010
"errors"
11+
"net"
1112
"net/netip"
1213
"slices"
1314
"sync/atomic"
@@ -34,9 +35,10 @@ type GWNoVia struct {
3435

3536
// GW is a no-op/stub gateway that is either dualstack or not and has dummy stats.
3637
type GW struct {
37-
nov4, nov6 bool // is dualstack
38-
stats x.RouterStats // zero stats
39-
since atomic.Int64 // uptime in unix millis
38+
nov4, nov6 bool // is dualstack
39+
stats x.RouterStats // zero stats
40+
since atomic.Int64 // uptime in unix millis
41+
lastaddr atomic.Pointer[string] // last dialed address (ip:port)
4042
}
4143

4244
// setSince resets the since time for this proxy (useful in re-add/update scenarios).
@@ -64,6 +66,19 @@ func (w *GW) Stat() *x.RouterStats {
6466
return &w.stats
6567
}
6668

69+
// Self implements x.Router.
70+
func (w *GW) Self(ip string) bool {
71+
if len(ip) <= 0 {
72+
return false
73+
}
74+
if a := w.lastaddr.Load(); a != nil {
75+
if host, _, err := net.SplitHostPort(*a); err == nil && host == ip {
76+
return true
77+
}
78+
}
79+
return false
80+
}
81+
6782
// Contains implements x.Router.
6883
func (w *GW) Contains(ippOrCidr string) bool {
6984
prefix, err := core.IP2Cidr2(ippOrCidr)
@@ -166,6 +181,7 @@ func (NoProxy) ID() string { return "
166181
func (NoProxy) Type() string { return "" }
167182
func (NoProxy) Router() x.Router { return nil }
168183
func (NoProxy) Reaches(string) bool { return false }
184+
func (NoProxy) Self(string) bool { return false }
169185
func (NoProxy) Dial(string, string) (protect.Conn, error) { return nil, errNop }
170186
func (NoProxy) DialBind(string, string, string) (protect.Conn, error) { return nil, errNop }
171187
func (NoProxy) Dialer() protect.RDialer { return nil }

0 commit comments

Comments
 (0)