-
Notifications
You must be signed in to change notification settings - Fork 220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: Workaround UDP port conflicts when another local process binds 53 #414
Merged
jschwinger233
merged 13 commits into
daeuniverse:main
from
jschwinger233:gray/fix-udp-port-conflict
Jan 11, 2024
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
a070b58
Handle transparent UDP socket in an independent netns
jschwinger233 559b8e5
Use netns only if EADDRINUSE occurred
jschwinger233 91e6e87
Concurrency control for indieNetns setup
jschwinger233 8c8c371
Add error details for setupIndieNetns
jschwinger233 27baf38
Support IPv6
jschwinger233 e1eba41
sysctl net.ipv4.conf.dae0.accept_local=1
jschwinger233 c1c967a
Add permanent ARP entry to avoid L2 resolution failure
jschwinger233 7a60fac
setupNetns can retry on errors
jschwinger233 818bb96
Monitor lladdr changes of dae0
jschwinger233 a3a1940
Control concurrency using sync.Mutex
jschwinger233 0e0fe1e
minor: adjust code style
jschwinger233 52252eb
Cleanup daens and veth for graceful termination
jschwinger233 b3f6ea4
Subscribe link events properly
jschwinger233 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,318 @@ | ||
package control | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"net" | ||
"os" | ||
"path" | ||
"runtime" | ||
"sync" | ||
"sync/atomic" | ||
|
||
"github.com/sirupsen/logrus" | ||
"github.com/vishvananda/netlink" | ||
"github.com/vishvananda/netns" | ||
"golang.org/x/sys/unix" | ||
) | ||
|
||
const ( | ||
NsName = "daens" | ||
HostVethName = "dae0" | ||
NsVethName = "dae0peer" | ||
) | ||
|
||
var ( | ||
daeNetns *DaeNetns | ||
) | ||
|
||
type DaeNetns struct { | ||
setupDone atomic.Bool | ||
mu sync.Mutex | ||
|
||
dae0, dae0peer netlink.Link | ||
hostNs, daeNs netns.NsHandle | ||
} | ||
|
||
func init() { | ||
daeNetns = &DaeNetns{} | ||
} | ||
|
||
func GetDaeNetns() *DaeNetns { | ||
return daeNetns | ||
} | ||
|
||
func (ns *DaeNetns) Setup() (err error) { | ||
if ns.setupDone.Load() { | ||
return | ||
} | ||
|
||
ns.mu.Lock() | ||
defer ns.mu.Unlock() | ||
if ns.setupDone.Load() { | ||
return | ||
} | ||
if err = ns.setup(); err != nil { | ||
return | ||
} | ||
ns.setupDone.Store(true) | ||
return nil | ||
} | ||
|
||
func (ns *DaeNetns) Close() (err error) { | ||
DeleteNamedNetns(NsName) | ||
DeleteLink(HostVethName) | ||
return | ||
} | ||
|
||
func (ns *DaeNetns) With(f func() error) (err error) { | ||
if err = daeNetns.Setup(); err != nil { | ||
return fmt.Errorf("failed to setup dae netns: %v", err) | ||
} | ||
|
||
runtime.LockOSThread() | ||
defer runtime.UnlockOSThread() | ||
|
||
if err = netns.Set(ns.daeNs); err != nil { | ||
return fmt.Errorf("failed to switch to daens: %v", err) | ||
} | ||
defer netns.Set(ns.hostNs) | ||
|
||
if err = f(); err != nil { | ||
return fmt.Errorf("failed to run func in dae netns: %v", err) | ||
} | ||
return | ||
} | ||
|
||
func (ns *DaeNetns) setup() (err error) { | ||
logrus.Trace("setting up dae netns") | ||
|
||
runtime.LockOSThread() | ||
defer runtime.UnlockOSThread() | ||
|
||
if ns.hostNs, err = netns.Get(); err != nil { | ||
return fmt.Errorf("failed to get host netns: %v", err) | ||
} | ||
defer netns.Set(ns.hostNs) | ||
|
||
if err = ns.setupVeth(); err != nil { | ||
return | ||
} | ||
if err = ns.setupSysctl(); err != nil { | ||
return | ||
} | ||
if err = ns.setupNetns(); err != nil { | ||
return | ||
} | ||
if err = ns.setupIPv4Datapath(); err != nil { | ||
return | ||
} | ||
if err = ns.setupIPv6Datapath(); err != nil { | ||
return | ||
} | ||
go ns.monitorDae0LinkAddr() | ||
return | ||
} | ||
|
||
func (ns *DaeNetns) setupVeth() (err error) { | ||
// ip l a dae0 type veth peer name dae0peer | ||
DeleteLink(HostVethName) | ||
if err = netlink.LinkAdd(&netlink.Veth{ | ||
LinkAttrs: netlink.LinkAttrs{ | ||
Name: HostVethName, | ||
}, | ||
PeerName: NsVethName, | ||
}); err != nil { | ||
return fmt.Errorf("failed to add veth pair: %v", err) | ||
} | ||
if ns.dae0, err = netlink.LinkByName(HostVethName); err != nil { | ||
return fmt.Errorf("failed to get link dae0: %v", err) | ||
} | ||
if ns.dae0peer, err = netlink.LinkByName(NsVethName); err != nil { | ||
return fmt.Errorf("failed to get link dae0peer: %v", err) | ||
} | ||
// ip l s dae0 up | ||
if err = netlink.LinkSetUp(ns.dae0); err != nil { | ||
return fmt.Errorf("failed to set link dae0 up: %v", err) | ||
} | ||
return | ||
} | ||
|
||
func (ns *DaeNetns) setupSysctl() (err error) { | ||
// sysctl net.ipv4.conf.dae0.rp_filter=0 | ||
if err = SetRpFilter(HostVethName, "0"); err != nil { | ||
return fmt.Errorf("failed to set rp_filter for dae0: %v", err) | ||
} | ||
// sysctl net.ipv4.conf.all.rp_filter=0 | ||
if err = SetRpFilter("all", "0"); err != nil { | ||
return fmt.Errorf("failed to set rp_filter for all: %v", err) | ||
} | ||
// sysctl net.ipv4.conf.dae0.arp_filter=0 | ||
if err = SetArpFilter(HostVethName, "0"); err != nil { | ||
return fmt.Errorf("failed to set arp_filter for dae0: %v", err) | ||
} | ||
// sysctl net.ipv4.conf.all.arp_filter=0 | ||
if err = SetArpFilter("all", "0"); err != nil { | ||
return fmt.Errorf("failed to set arp_filter for all: %v", err) | ||
} | ||
// sysctl net.ipv4.conf.dae0.accept_local=1 | ||
if err = SetAcceptLocal(HostVethName, "1"); err != nil { | ||
return fmt.Errorf("failed to set accept_local for dae0: %v", err) | ||
} | ||
// sysctl net.ipv6.conf.dae0.disable_ipv6=0 | ||
if err = SetDisableIpv6(HostVethName, "0"); err != nil { | ||
return fmt.Errorf("failed to set disable_ipv6 for dae0: %v", err) | ||
} | ||
// sysctl net.ipv6.conf.dae0.forwarding=1 | ||
SetForwarding(HostVethName, "1") | ||
// sysctl net.ipv6.conf.all.forwarding=1 | ||
SetForwarding("all", "1") | ||
return | ||
} | ||
|
||
func (ns *DaeNetns) setupNetns() (err error) { | ||
// ip netns a daens | ||
DeleteNamedNetns(NsName) | ||
ns.daeNs, err = netns.NewNamed(NsName) | ||
if err != nil { | ||
return fmt.Errorf("failed to create netns: %v", err) | ||
} | ||
// NewNamed() will switch to the new netns, switch back to host netns | ||
if err = netns.Set(ns.hostNs); err != nil { | ||
return fmt.Errorf("failed to switch to host netns: %v", err) | ||
} | ||
// ip l s dae0peer netns daens | ||
if err = netlink.LinkSetNsFd(ns.dae0peer, int(ns.daeNs)); err != nil { | ||
return fmt.Errorf("failed to move dae0peer to daens: %v", err) | ||
} | ||
return | ||
} | ||
|
||
func (ns *DaeNetns) setupIPv4Datapath() (err error) { | ||
if err = netns.Set(ns.daeNs); err != nil { | ||
return fmt.Errorf("failed to switch to daens: %v", err) | ||
} | ||
defer netns.Set(ns.hostNs) | ||
|
||
// (ip net e daens) ip l s dae0peer up | ||
if err = netlink.LinkSetUp(ns.dae0peer); err != nil { | ||
return fmt.Errorf("failed to set link dae0peer up: %v", err) | ||
} | ||
// (ip net e daens) ip a a 169.254.0.11 dev dae0peer | ||
// Although transparent UDP socket doesn't use this IP, it's still needed to make proper L3 header | ||
ip, ipNet, err := net.ParseCIDR("169.254.0.11/32") | ||
ipNet.IP = ip | ||
if err != nil { | ||
return fmt.Errorf("failed to parse ip 169.254.0.11: %v", err) | ||
} | ||
if err = netlink.AddrAdd(ns.dae0peer, &netlink.Addr{IPNet: ipNet}); err != nil { | ||
return fmt.Errorf("failed to add v4 addr to dae0peer: %v", err) | ||
} | ||
// (ip net e daens) ip r a 169.254.0.1 dev dae0peer | ||
// 169.254.0.1 is the link-local address used for ARP caching | ||
if err = netlink.RouteAdd(&netlink.Route{ | ||
LinkIndex: ns.dae0peer.Attrs().Index, | ||
Dst: &net.IPNet{IP: net.ParseIP("169.254.0.1"), Mask: net.CIDRMask(32, 32)}, | ||
Gw: nil, | ||
Scope: netlink.SCOPE_LINK, | ||
}); err != nil { | ||
return fmt.Errorf("failed to add v4 route1 to dae0peer: %v", err) | ||
} | ||
// (ip net e daens) ip r a default via 169.254.0.1 dev dae0peer | ||
if err = netlink.RouteAdd(&netlink.Route{ | ||
LinkIndex: ns.dae0peer.Attrs().Index, | ||
Dst: &net.IPNet{IP: net.IPv4(0, 0, 0, 0), Mask: net.CIDRMask(0, 32)}, | ||
Gw: net.ParseIP("169.254.0.1"), | ||
}); err != nil { | ||
return fmt.Errorf("failed to add v4 route2 to dae0peer: %v", err) | ||
} | ||
// (ip net e daens) ip n r 169.254.0.1 dev dae0peer lladdr $mac_dae0 nud permanent | ||
return | ||
} | ||
|
||
func (ns *DaeNetns) setupIPv6Datapath() (err error) { | ||
// ip -6 a a fe80::ecee:eeff:feee:eeee/128 dev dae0 scope link | ||
// fe80::ecee:eeff:feee:eeee/128 is the link-local address used for L2 NDP addressing | ||
if err = netlink.AddrAdd(ns.dae0, &netlink.Addr{ | ||
IPNet: &net.IPNet{ | ||
IP: net.ParseIP("fe80::ecee:eeff:feee:eeee"), | ||
Mask: net.CIDRMask(128, 128), | ||
}, | ||
}); err != nil { | ||
return fmt.Errorf("failed to add v6 addr to dae0: %v", err) | ||
} | ||
|
||
if err = netns.Set(ns.daeNs); err != nil { | ||
return fmt.Errorf("failed to switch to daens: %v", err) | ||
} | ||
defer netns.Set(ns.hostNs) | ||
|
||
// (ip net e daens) ip -6 r a default via fe80::ecee:eeff:feee:eeee dev dae0peer | ||
if err = netlink.RouteAdd(&netlink.Route{ | ||
LinkIndex: ns.dae0peer.Attrs().Index, | ||
Dst: &net.IPNet{IP: net.IPv6zero, Mask: net.CIDRMask(0, 128)}, | ||
Gw: net.ParseIP("fe80::ecee:eeff:feee:eeee"), | ||
}); err != nil { | ||
return fmt.Errorf("failed to add v6 route to dae0peer: %v", err) | ||
} | ||
return | ||
} | ||
|
||
// updateNeigh() isn't named as setupNeigh() because it requires runtime.LockOSThread() | ||
func (ns *DaeNetns) updateNeigh() (err error) { | ||
runtime.LockOSThread() | ||
defer runtime.UnlockOSThread() | ||
|
||
if err = netns.Set(ns.daeNs); err != nil { | ||
return fmt.Errorf("failed to switch to daens: %v", err) | ||
} | ||
defer netns.Set(ns.hostNs) | ||
|
||
if err = netlink.NeighSet(&netlink.Neigh{ | ||
IP: net.ParseIP("169.254.0.1"), | ||
HardwareAddr: ns.dae0.Attrs().HardwareAddr, | ||
LinkIndex: ns.dae0peer.Attrs().Index, | ||
State: netlink.NUD_PERMANENT, | ||
}); err != nil { | ||
return fmt.Errorf("failed to add neigh to dae0peer: %v", err) | ||
} | ||
return | ||
} | ||
|
||
func (ns *DaeNetns) monitorDae0LinkAddr() { | ||
ch := make(chan netlink.LinkUpdate) | ||
done := make(chan struct{}) | ||
defer close(done) | ||
|
||
err := netlink.LinkSubscribe(ch, done) | ||
if err != nil { | ||
logrus.Errorf("failed to subscribe link updates: %v", err) | ||
} | ||
if ns.dae0, err = netlink.LinkByName(HostVethName); err != nil { | ||
logrus.Errorf("failed to get link dae0: %v", err) | ||
} | ||
if err = ns.updateNeigh(); err != nil { | ||
logrus.Errorf("failed to update neigh: %v", err) | ||
} | ||
for msg := range ch { | ||
if msg.Link.Attrs().Name == HostVethName && !bytes.Equal(msg.Link.Attrs().HardwareAddr, ns.dae0.Attrs().HardwareAddr) { | ||
logrus.WithField("old addr", ns.dae0.Attrs().HardwareAddr).WithField("new addr", msg.Link.Attrs().HardwareAddr).Info("dae0 link addr changed") | ||
ns.dae0 = msg.Link | ||
ns.updateNeigh() | ||
} | ||
} | ||
} | ||
|
||
func DeleteNamedNetns(name string) error { | ||
namedPath := path.Join("/run/netns", name) | ||
unix.Unmount(namedPath, unix.MNT_DETACH|unix.MNT_FORCE) | ||
return os.Remove(namedPath) | ||
} | ||
|
||
func DeleteLink(name string) error { | ||
link, err := netlink.LinkByName(name) | ||
if err == nil { | ||
return netlink.LinkDel(link) | ||
} | ||
return err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to implement
func Close() error
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Who calls this Close()?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should figure out that daens is within the program lifecycle or the control plane lifecycle. I think it should be within the program lifecycle to avoid data interruption when reloading.
Reloading closes the old control plane and structs a new control plane. If daens is within the control plane, interfaces will be recreated and unexpected interruptions could happen while reloading.
If daens is within the program lifecycle, we should make sure the interface dae0 is removed before destroying (SIGINT and normally quit).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thought about the lifecycle idea but gave it up, because
sendPkt
is an independent function without access to *ControlPlane.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is to say, we can add
defer daeNs.Close()
at:dae/cmd/run.go
Line 115 in 8beaa5b