288 lines
7.1 KiB
Go
288 lines
7.1 KiB
Go
package netcfg
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"flag"
|
||
"fmt"
|
||
"log"
|
||
"net"
|
||
"net/netip"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"syscall"
|
||
"time"
|
||
|
||
"github.com/vishvananda/netlink"
|
||
"go4.org/netipx"
|
||
)
|
||
|
||
var defaultDst = func() *net.IPNet {
|
||
_, net, err := net.ParseCIDR("0.0.0.0/0")
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
return net
|
||
}()
|
||
|
||
func priorityFromName(ifname string) int {
|
||
if strings.HasPrefix(ifname, "eth") {
|
||
return 1
|
||
}
|
||
return 5 // wlan0 and others
|
||
}
|
||
|
||
func applyCFG(nl *netlink.Handle, ifname string, source string, cfg Config, extraRoutePriority int) error {
|
||
// Log the received DHCPACK packet:
|
||
details := []string{
|
||
fmt.Sprintf("IP %v", cfg.CIDR),
|
||
}
|
||
if len(cfg.Router) > 0 {
|
||
details = append(details, fmt.Sprintf("router %v", cfg.Router))
|
||
}
|
||
if len(cfg.DNS) > 0 {
|
||
details = append(details, fmt.Sprintf("DNS %v", cfg.DNS))
|
||
}
|
||
|
||
log.Printf("%s: %v", source, strings.Join(details, ", "))
|
||
|
||
l, err := nl.LinkByName(ifname)
|
||
if err != nil {
|
||
return fmt.Errorf("LinkByName: %v", err)
|
||
}
|
||
|
||
|
||
// Apply the received settings:
|
||
if err := nl.AddrReplace(l, &netlink.Addr{IPNet: netipx.PrefixIPNet(cfg.CIDR)}); err != nil {
|
||
return fmt.Errorf("AddrReplace: %v", err)
|
||
}
|
||
|
||
if l.Attrs().OperState != netlink.OperUp {
|
||
if err := nl.LinkSetUp(l); err != nil {
|
||
return fmt.Errorf("LinkSetUp: %v", err)
|
||
}
|
||
}
|
||
|
||
// Adjust the priority of the network routes on this interface; the kernel
|
||
// adds at least one based on the configured address.
|
||
if err := changeRoutePriority(nl, l, priorityFromName(ifname)+extraRoutePriority); err != nil {
|
||
return fmt.Errorf("changeRoutePriority: %v", err)
|
||
}
|
||
|
||
if r := cfg.Router; len(r) > 0 {
|
||
err = nl.RouteReplace(&netlink.Route{
|
||
LinkIndex: l.Attrs().Index,
|
||
Dst: defaultDst,
|
||
Gw: r,
|
||
Priority: priorityFromName(ifname) + extraRoutePriority,
|
||
})
|
||
if err != nil {
|
||
return fmt.Errorf("RouteReplace: %v", err)
|
||
}
|
||
}
|
||
|
||
if len(cfg.DNS) > 0 {
|
||
if err := writeResolvConf(cfg); err != nil {
|
||
return fmt.Errorf("writing resolv.conf: %v", err)
|
||
}
|
||
}
|
||
|
||
// Notify init of new addresses
|
||
p, _ := os.FindProcess(1)
|
||
if err := p.Signal(syscall.SIGHUP); err != nil {
|
||
log.Printf("send SIGHUP to init: %v", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func writeResolvConf(lease Config) error {
|
||
const resolvConf = "/tmp/resolv.conf"
|
||
|
||
// Has another program (e.g. tailscale) replaced our resolv.conf?
|
||
// Note: at boot, /tmp/resolv.conf is a symlink to /proc/net/pnp,
|
||
// which contains no “generated by” marker.
|
||
if b, err := os.ReadFile(resolvConf); err == nil {
|
||
if bytes.Contains(b, []byte("generated by")) &&
|
||
!bytes.Contains(b, []byte("generated by netcfg")) {
|
||
log.Printf("%s updated outside of netcfg, not updating", resolvConf)
|
||
return nil
|
||
}
|
||
}
|
||
|
||
lines := []string{
|
||
"# generated by netcfg",
|
||
}
|
||
if domain := lease.Domain; domain != "" {
|
||
lines = append(lines, fmt.Sprintf("domain %s", domain))
|
||
lines = append(lines, fmt.Sprintf("search %s", domain))
|
||
}
|
||
for _, ns := range lease.DNS {
|
||
lines = append(lines, fmt.Sprintf("nameserver %v", ns))
|
||
}
|
||
if err := os.WriteFile(resolvConf, []byte(strings.Join(lines, "\n")+"\n"), 0o644); err != nil {
|
||
return fmt.Errorf("resolv.conf: %v", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func changeRoutePriority(nl *netlink.Handle, l netlink.Link, priority int) error {
|
||
routes, err := nl.RouteList(l, netlink.FAMILY_V4)
|
||
if err != nil {
|
||
return fmt.Errorf("netlink.RouteList: %v", err)
|
||
}
|
||
for _, route := range routes {
|
||
if route.Priority == priority {
|
||
continue // no change necessary
|
||
}
|
||
newRoute := route // copy
|
||
log.Printf("adjusting route [dst=%v src=%v gw=%v] priority to %d", route.Dst, route.Src, route.Gw, priority)
|
||
newRoute.Flags = 0 // prevent "invalid argument" error
|
||
newRoute.Priority = priority
|
||
if err := nl.RouteReplace(&newRoute); err != nil {
|
||
return fmt.Errorf("RouteReplace: %v", err)
|
||
}
|
||
if err := nl.RouteDel(&route); err != nil {
|
||
return fmt.Errorf("RouteDel: %v", err)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func deprioritizeRoutesWhenDown(nl *netlink.Handle, ifname string, extraRoutePriority int) {
|
||
last := netlink.LinkOperState(netlink.OperUp)
|
||
for range time.Tick(1 * time.Second) {
|
||
l, err := nl.LinkByName(ifname)
|
||
if err != nil {
|
||
log.Printf("netlink.LinkByName: %v", err)
|
||
continue
|
||
}
|
||
operState := l.Attrs().OperState
|
||
if last == operState {
|
||
continue // no change
|
||
}
|
||
last = operState
|
||
if operState == netlink.OperDown {
|
||
log.Printf("lost carrier on interface %s, de-prioritizing routes", ifname)
|
||
if err := changeRoutePriority(nl, l, 1024); err != nil {
|
||
log.Print(err)
|
||
}
|
||
} else {
|
||
log.Printf("regained carrier on interface %s, re-prioritizing routes", ifname)
|
||
if err := changeRoutePriority(nl, l, priorityFromName(ifname)+extraRoutePriority); err != nil {
|
||
log.Print(err)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func waitForInterface(ifname string) {
|
||
t := time.NewTicker(1 * time.Second)
|
||
defer t.Stop()
|
||
|
||
log.Printf("waiting indefinitely for %s to appear", ifname)
|
||
for range t.C {
|
||
if _, err := net.InterfaceByName(ifname); err == nil {
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
type Config struct {
|
||
CIDR netip.Prefix
|
||
Router net.IP
|
||
VlanID int
|
||
DNS []net.IP
|
||
Domain string
|
||
}
|
||
|
||
func Main() {
|
||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||
var (
|
||
ifname = flag.String(
|
||
"interface",
|
||
"eth0",
|
||
"network interface to obtain a DHCP lease on")
|
||
|
||
staticConfig = flag.String(
|
||
"static_network_config",
|
||
"",
|
||
"network configuration to apply instead of querying DHCP")
|
||
|
||
extraRoutePriority = flag.Int(
|
||
"extra_route_priority",
|
||
0,
|
||
"extra value to add to the interface’s priority (eth* defaults to priority 1, wlan* defaults to priority 5, interfaces without link are set to priority 1024)")
|
||
)
|
||
flag.Parse()
|
||
|
||
// NOTE: cannot gokrazy.WaitForClock() here, since the clock can only be
|
||
// initialized once the network is up.
|
||
|
||
waitForInterface(*ifname)
|
||
|
||
nl, err := netlink.NewHandle(netlink.FAMILY_V4)
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
go deprioritizeRoutesWhenDown(nl, *ifname, *extraRoutePriority)
|
||
|
||
|
||
// Ensure the interface is up.
|
||
link, err := nl.LinkByName(*ifname)
|
||
if err != nil {
|
||
log.Fatalf("LinkByName: %v", err)
|
||
}
|
||
if link.Attrs().OperState != netlink.OperUp {
|
||
if err := nl.LinkSetUp(link); err != nil {
|
||
log.Fatalf("LinkSetUp: %v", err)
|
||
}
|
||
}
|
||
|
||
// Wait for up to 10 seconds for the link to indicate it has a
|
||
// carrier.
|
||
for range 10 {
|
||
b, err := os.ReadFile(filepath.Join("/sys/class/net", *ifname, "carrier"))
|
||
if err == nil && strings.TrimSpace(string(b)) == "1" {
|
||
break
|
||
}
|
||
time.Sleep(1 * time.Second)
|
||
}
|
||
var sc []byte
|
||
var lsrc string
|
||
|
||
// Simple heuristic to check if we got JSON strings from the command line
|
||
if strings.HasPrefix(strings.TrimSpace(*staticConfig), "{") {
|
||
sc = []byte(*staticConfig)
|
||
lsrc = "-static_network_config <string>"
|
||
} else {
|
||
sc, err = os.ReadFile(*staticConfig)
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
|
||
lsrc = fmt.Sprintf("-static_network_config=%s", *staticConfig)
|
||
}
|
||
|
||
var l Config
|
||
if err := json.Unmarshal(sc, &l); err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
if l.Router != nil {
|
||
l.Router = l.Router.To4()
|
||
}
|
||
for idx, dns := range l.DNS {
|
||
l.DNS[idx] = dns.To4()
|
||
}
|
||
|
||
l.CIDR.Addr()
|
||
for {
|
||
if err := applyCFG(nl, *ifname, lsrc, l, *extraRoutePriority); err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
log.Print("Static config applied successfully")
|
||
time.Sleep(15*time.Second)
|
||
}
|
||
}
|