Add IPv6 and VLAN devices to the web UI and router-setup
The web UI lists physical and VLAN interfaces and takes a VLAN uplink as a device name (enp2s0.1001); it has IPv6 checkboxes for the WAN and each LAN. router-setup asks whether to create VLANs before WAN and LAN are picked, and whether to enable IPv6. --config-only writes the configuration without installing, which the setup test uses for a PPPoE uplink on a VLAN. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
@@ -131,10 +131,10 @@ type Form struct {
|
||||
}
|
||||
|
||||
type WANForm struct {
|
||||
Interface string
|
||||
Interface string // a device: "enp2s0", or "enp2s0.1001" for a VLAN
|
||||
Type string // "dhcp" or "pppoe"
|
||||
IPv6 bool
|
||||
PPPoEUser string
|
||||
PPPoEVLAN string
|
||||
PPPoEPassword string // write-only: empty keeps the stored one
|
||||
PasswordSet bool
|
||||
}
|
||||
@@ -146,6 +146,7 @@ type LANForm struct {
|
||||
Address string
|
||||
Prefix string
|
||||
Internet bool
|
||||
IPv6PD bool // delegate the ISP's prefix to this LAN
|
||||
DHCP bool
|
||||
PoolStart string
|
||||
PoolEnd string
|
||||
@@ -162,10 +163,13 @@ func (s Settings) form() Form {
|
||||
WAN: WANForm{
|
||||
Interface: str(router, "wan", "interface"),
|
||||
Type: str(router, "wan", "ipv4", "type"),
|
||||
IPv6: boolean(router, false, "wan", "ipv6", "enable"),
|
||||
PPPoEUser: str(router, "wan", "ipv4", "pppoe", "username"),
|
||||
PPPoEVLAN: str(router, "wan", "ipv4", "pppoe", "vlan"),
|
||||
},
|
||||
}
|
||||
if vlan := str(router, "wan", "ipv4", "pppoe", "vlan"); vlan != "" && f.WAN.Type == "pppoe" {
|
||||
f.WAN.Interface += "." + vlan
|
||||
}
|
||||
if f.WAN.Type == "" {
|
||||
f.WAN.Type = "dhcp"
|
||||
}
|
||||
@@ -188,6 +192,7 @@ func (s Settings) form() Form {
|
||||
Address: str(l, "ipv4", "address"),
|
||||
Prefix: str(l, "ipv4", "prefixLength"),
|
||||
Internet: boolean(l, false, "internet"),
|
||||
IPv6PD: boolean(l, false, "ipv6", "pd"),
|
||||
DHCP: boolean(l, true, "dhcp", "enable"),
|
||||
PoolStart: str(l, "dhcp", "pool", "start"),
|
||||
PoolEnd: str(l, "dhcp", "pool", "end"),
|
||||
@@ -229,6 +234,64 @@ func optionalInt(v string, lo, hi int) (int, bool, error) {
|
||||
return n, true, nil
|
||||
}
|
||||
|
||||
// splitVLAN splits "enp2s0.1001" into the interface and its VLAN ID; a name
|
||||
// without one has VLAN 0.
|
||||
func splitVLAN(dev string) (string, int) {
|
||||
dev = strings.TrimSpace(dev)
|
||||
i := strings.LastIndex(dev, ".")
|
||||
if i <= 0 {
|
||||
return dev, 0
|
||||
}
|
||||
id, err := strconv.Atoi(dev[i+1:])
|
||||
if err != nil || id < 1 || id > 4094 {
|
||||
return dev, 0
|
||||
}
|
||||
return dev[:i], id
|
||||
}
|
||||
|
||||
// Device is a row of the interface list: a physical NIC or a VLAN on one.
|
||||
type Device struct {
|
||||
Name string
|
||||
Kind string // "physical" or "VLAN"
|
||||
Role string // "WAN", "LAN lan", or ""
|
||||
}
|
||||
|
||||
// devices lists the physical interfaces plus the VLAN devices the settings
|
||||
// use, each with the role it plays.
|
||||
func (f Form) devices(physical []string) []Device {
|
||||
roles := map[string][]string{}
|
||||
add := func(dev, role string) { roles[dev] = append(roles[dev], role) }
|
||||
add(f.WAN.Interface, "WAN")
|
||||
for _, l := range f.LANs {
|
||||
if l.Name == "" || l.Remove {
|
||||
continue
|
||||
}
|
||||
dev := l.Interface
|
||||
if l.VLAN != "" {
|
||||
dev += "." + l.VLAN
|
||||
}
|
||||
add(dev, "LAN "+l.Name)
|
||||
}
|
||||
names := slices.Clone(physical)
|
||||
var vlans []string
|
||||
for dev := range roles {
|
||||
if _, id := splitVLAN(dev); id != 0 && !slices.Contains(names, dev) {
|
||||
vlans = append(vlans, dev)
|
||||
}
|
||||
}
|
||||
slices.Sort(vlans)
|
||||
var out []Device
|
||||
for _, n := range names {
|
||||
out = append(out, Device{Name: n, Kind: "physical", Role: strings.Join(roles[n], ", ")})
|
||||
for _, v := range vlans {
|
||||
if parent, _ := splitVLAN(v); parent == n {
|
||||
out = append(out, Device{Name: v, Kind: "VLAN", Role: strings.Join(roles[v], ", ")})
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func ipv4(v string) (netip.Addr, bool) {
|
||||
a, err := netip.ParseAddr(strings.TrimSpace(v))
|
||||
return a, err == nil && a.Is4()
|
||||
@@ -250,11 +313,17 @@ func (s Settings) apply(f Form, passwordFile string) (string, error) {
|
||||
system["hostName"] = f.HostName
|
||||
}
|
||||
|
||||
// A VLAN device (enp2s0.1001) is the physical interface plus the
|
||||
// PPPoE session's VLAN.
|
||||
wanDev, wanVLAN := splitVLAN(f.WAN.Interface)
|
||||
if !ifaceRe.MatchString(f.WAN.Interface) {
|
||||
errs["wan.interface"] = "an interface name is required"
|
||||
}
|
||||
wan["interface"] = f.WAN.Interface
|
||||
wan["interface"] = wanDev
|
||||
ipv4cfg := obj(wan, "ipv4")
|
||||
if wanVLAN != 0 && f.WAN.Type != "pppoe" {
|
||||
errs["wan.interface"] = "an uplink on a VLAN needs PPPoE"
|
||||
}
|
||||
switch f.WAN.Type {
|
||||
case "dhcp":
|
||||
ipv4cfg["type"] = "dhcp"
|
||||
@@ -265,10 +334,8 @@ func (s Settings) apply(f Form, passwordFile string) (string, error) {
|
||||
if pppoe["username"] == "" {
|
||||
errs["wan.pppoe.username"] = "required for PPPoE"
|
||||
}
|
||||
if vlan, ok, err := optionalInt(f.WAN.PPPoEVLAN, 1, 4094); err != nil {
|
||||
errs["wan.pppoe.vlan"] = err.Error()
|
||||
} else if ok {
|
||||
pppoe["vlan"] = vlan
|
||||
if wanVLAN != 0 {
|
||||
pppoe["vlan"] = wanVLAN
|
||||
} else {
|
||||
delete(pppoe, "vlan")
|
||||
}
|
||||
@@ -280,6 +347,18 @@ func (s Settings) apply(f Form, passwordFile string) (string, error) {
|
||||
default:
|
||||
errs["wan.type"] = "must be dhcp or pppoe"
|
||||
}
|
||||
if f.WAN.IPv6 {
|
||||
v6 := obj(wan, "ipv6")
|
||||
v6["enable"] = true
|
||||
// PPPoE carries IPv6 inside the session; otherwise it is native.
|
||||
if f.WAN.Type == "pppoe" {
|
||||
v6["transport"] = "ppp"
|
||||
} else {
|
||||
v6["transport"] = "native"
|
||||
}
|
||||
} else if v6, ok := wan["ipv6"].(map[string]any); ok {
|
||||
v6["enable"] = false
|
||||
}
|
||||
|
||||
lans := obj(router, "lans")
|
||||
keep := map[string]bool{}
|
||||
@@ -314,6 +393,14 @@ func (s Settings) apply(f Form, passwordFile string) (string, error) {
|
||||
delete(l, "vlan")
|
||||
}
|
||||
l["internet"] = lf.Internet
|
||||
if lf.IPv6PD {
|
||||
obj(l, "ipv6")["pd"] = true
|
||||
} else if v6, ok := l["ipv6"].(map[string]any); ok {
|
||||
delete(v6, "pd")
|
||||
if len(v6) == 0 {
|
||||
delete(l, "ipv6")
|
||||
}
|
||||
}
|
||||
v4 := obj(l, "ipv4")
|
||||
if _, ok := ipv4(lf.Address); !ok {
|
||||
errs[field("address")] = "an IPv4 address is required"
|
||||
|
||||
Reference in New Issue
Block a user