#!/usr/bin/perl ############################################################################### # # # VLAN Management for IPFire # # Copyright (C) 2019 Florian Bührle # # # # This program is free software: you can redistribute it and/or modify # # it under the terms of the GNU General Public License as published by # # the Free Software Foundation, either version 3 of the License, or # # (at your option) any later version. # # # # This program is distributed in the hope that it will be useful, # # but WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # # GNU General Public License for more details. # # # # You should have received a copy of the GNU General Public License # # along with this program. If not, see . # # # ############################################################################### use strict; use Scalar::Util qw(looks_like_number); require '/var/ipfire/general-functions.pl'; require "${General::swroot}/lang.pl"; require "${General::swroot}/header.pl"; require "${General::swroot}/network-functions.pl"; ###--- HTML HEAD ---### my $extraHead = < table#zoneconf { width: 100%; border-collapse: collapse; border-style: hidden; table-layout: fixed; } /* row height */ #zoneconf tr { height: 4em; } #zoneconf tr.half-height { height: 2em; } #zoneconf tr.half-height > td { padding: 2px 10px; } /* section separators */ #zoneconf tr.divider-top { border-top: 2px solid $Header::bordercolour; } #zoneconf tr.divider-bottom { border-bottom: 2px solid $Header::bordercolour; } /* table cells */ #zoneconf td { padding: 5px 10px; border-left: 0.5px solid $Header::bordercolour; text-align: center; } /* grey header cells */ #zoneconf td.heading { background-color: lightgrey; color: white; } #zoneconf td.heading.bold::first-line { font-weight: bold; line-height: 1.6; } /* narrow left column with background color */ #zoneconf tr > td:first-child { width: 11em; } #zoneconf tr.nic-row > td:first-child { background-color: darkgray; } #zoneconf tr.nic-row { border-bottom: 0.5px solid $Header::bordercolour; } #zoneconf tr.option-row > td:first-child { background-color: gray; } /* alternating row background color */ #zoneconf tr { background-color: $Header::table2colour; } #zoneconf tr:nth-child(2n+3) { background-color: $Header::table1colour; } /* special cell colors */ #zoneconf td.green { background-color: $Header::colourgreen; } #zoneconf td.red { background-color: $Header::colourred; } #zoneconf td.blue { background-color: $Header::colourblue; } #zoneconf td.orange { background-color: $Header::colourorange; } #zoneconf td.topleft { background-color: $Header::pagecolour; } input.vlanid { width: 4em; } input.stp-priority { width: 5em; } #submit-container { width: 100%; padding-top: 20px; text-align: right; color: red; } #submit-container.input { margin-left: auto; } END ; ###--- END HTML HEAD ---### ### Read configuration ### my %ethsettings = (); my %vlansettings = (); my %cgiparams = (); my $restart_notice = ""; &General::readhash("${General::swroot}/ethernet/settings",\%ethsettings); &General::readhash("${General::swroot}/ethernet/vlans",\%vlansettings); &Header::getcgihash(\%cgiparams); &Header::showhttpheaders(); # Get all network zones that are currently enabled my @zones = Network::get_available_network_zones(); # Get all physical NICs present opendir(my $dh, "/sys/class/net/"); my @nics = (); while (my $nic = readdir($dh)) { if (-e "/sys/class/net/$nic/device") { # Indicates that the NIC is physical push(@nics, [&Network::get_nic_property($nic, "address"), $nic, 0]); } } closedir($dh); @nics = sort {$a->[0] cmp $b->[0]} @nics; # Sort nics by their MAC address # Name the physical NICs # Even though they may not be really named like this, we will name them ethX or wlanX my $ethcount = 0; my $wlancount = 0; foreach (@nics) { my $nic = $_->[1]; if (-e "/sys/class/net/$nic/wireless") { $_->[1] = "wlan$wlancount"; $_->[2] = 1; $wlancount++; } else { $_->[1] = "eth$ethcount"; $ethcount++; } } ### START PAGE ### &Header::openpage($Lang::tr{"zoneconf title"}, 1, $extraHead); &Header::openbigbox('100%', 'center'); ### Evaluate POST parameters ### if ($cgiparams{"ACTION"} eq $Lang::tr{"save"}) { my %VALIDATE_nic_check = (); # array of flags (assigned, restricted/pppoe, vlan, ...) per NIC my $VALIDATE_error = ""; # contains an error message if the config validation failed # Loop trough all known zones to ensure a complete configuration file is created foreach (@Network::known_network_zones) { my $uc = uc $_; my $slave_string = ""; # list of interfaces attached to the bridge my $zone_mode = $cgiparams{"MODE $uc"}; my $VALIDATE_vlancount = 0; my $VALIDATE_zoneslaves = 0; # Each zone can contain up to one bridge and up to one VLAN, # cache their mac addresses to prevent unnecessary changes my $bridge_mac = $ethsettings{"${uc}_MACADDR"}; my $vlan_mac = $vlansettings{"${uc}_MAC_ADDRESS"}; # Clear old configuration $ethsettings{"${uc}_MACADDR"} = ""; $ethsettings{"${uc}_MODE"} = ""; $ethsettings{"${uc}_SLAVES"} = ""; $vlansettings{"${uc}_PARENT_DEV"} = ""; $vlansettings{"${uc}_VLAN_ID"} = ""; $vlansettings{"${uc}_MAC_ADDRESS"} = ""; # If RED is not in DHCP or static mode, we only set its MACADDR property if ($uc eq "RED" && ! $cgiparams{"PPPACCESS"} eq "") { foreach (@nics) { my $mac = $_->[0]; if ($mac eq $cgiparams{"PPPACCESS"}) { $ethsettings{"${uc}_MACADDR"} = $mac; # Check if this interface is already accessed by any other zone # If this is the case, show an error message if ($VALIDATE_nic_check{"ACC $mac"}) { $VALIDATE_error = $Lang::tr{"zoneconf val ppp assignment error"}; } $VALIDATE_nic_check{"RESTRICT $mac"} = 1; last; } } # skip NIC/VLAN assignment and additional zone options for RED in PPP mode next; } # Zone in bridge mode: Always assign a MAC to the bridge if($zone_mode eq "BRIDGE") { # Ensure that the bridge's cached MAC does not come from a real NIC # (this could happen if the zone was in default mode before) foreach (@nics) { my $nic_mac = $_->[0]; if(Network::is_mac_equal($bridge_mac, $nic_mac)) { $bridge_mac = ""; last; } } # Generate random MAC if none was configured if(! Network::valid_mac($bridge_mac)) { $bridge_mac = Network::random_mac(); } # Assign the address to the bridge $ethsettings{"${uc}_MACADDR"} = $bridge_mac; } foreach (@nics) { my $mac = $_->[0]; my $nic_access = $cgiparams{"ACCESS $uc $mac"}; next unless ($nic_access); # This NIC is to be assigned: check preconditions if ($nic_access ne "NONE") { if ($VALIDATE_nic_check{"RESTRICT $mac"}) { # If this interface is already assigned to RED in PPP mode, throw an error $VALIDATE_error = $Lang::tr{"zoneconf val ppp assignment error"}; last; } # Enforce bridge mode when you try to assign multiple NICs to a zone if ($zone_mode ne "BRIDGE" && $VALIDATE_zoneslaves > 0 && $nic_access ne "") { $VALIDATE_error = $Lang::tr{"zoneconf val zoneslave amount error"}; last; } # Mark this NIC as "accessed by zone" $VALIDATE_nic_check{"ACC $mac"} = 1; $VALIDATE_zoneslaves++; } if ($nic_access eq "NATIVE") { if ($VALIDATE_nic_check{"NATIVE $mac"}) { $VALIDATE_error = $Lang::tr{"zoneconf val native assignment error"}; last; } $VALIDATE_nic_check{"NATIVE $mac"} = 1; # Zone in bridge mode: Add NIC to slave list. Otherwise access NIC directly if ($zone_mode eq "BRIDGE") { $slave_string = "${slave_string}${mac} "; } else { $ethsettings{"${uc}_MACADDR"} = $mac; } } elsif ($nic_access eq "VLAN") { my $vlan_tag = $cgiparams{"TAG $uc $mac"}; if ($VALIDATE_nic_check{"VLAN $mac $vlan_tag"}) { $VALIDATE_error = $Lang::tr{"zoneconf val vlan tag assignment error"}; last; } $VALIDATE_nic_check{"VLAN $mac $vlan_tag"} = 1; # check VLAN tag range: 1..4094 (0, 4095 are reserved) unless (looks_like_number($vlan_tag) && ($vlan_tag >= 1) && ($vlan_tag <= 4094)) { $VALIDATE_error = $Lang::tr{"zoneconf val vlan tag range error"}; last; } # Generate random MAC if none was configured if(! Network::valid_mac($vlan_mac)) { $vlan_mac = Network::random_mac(); } $vlansettings{"${uc}_PARENT_DEV"} = $mac; $vlansettings{"${uc}_VLAN_ID"} = $vlan_tag; $vlansettings{"${uc}_MAC_ADDRESS"} = $vlan_mac; # Generated MAC # Zone in bridge mode: Add VLAN to slave list if ($zone_mode eq "BRIDGE") { $slave_string = "${slave_string}${vlan_mac} "; } $VALIDATE_vlancount++; # We can't allow more than one VLAN per zone } } if ($VALIDATE_vlancount > 1) { $VALIDATE_error = $Lang::tr{"zoneconf val vlan amount assignment error"}; last; } chop($slave_string); if ($zone_mode eq "BRIDGE") { $ethsettings{"${uc}_MODE"} = "bridge"; $ethsettings{"${uc}_SLAVES"} = $slave_string; } # STP options # (this has already been skipped when RED is in PPP mode, so we don't need to check for PPP here) $ethsettings{"${uc}_STP"} = ""; my $stp_enabled = $cgiparams{"STP-$uc"} eq "on"; my $stp_priority = $cgiparams{"STP-PRIORITY-$uc"}; if($stp_enabled) { unless($ethsettings{"${uc}_MODE"} eq "bridge") { # STP is only available in bridge mode $VALIDATE_error = $Lang::tr{"zoneconf val stp zone mode error"}; last; } unless (looks_like_number($stp_priority) && ($stp_priority >= 1) && ($stp_priority <= 65535)) { # STP bridge priority range: 1..65535 $VALIDATE_error = $Lang::tr{"zoneconf val stp priority range error"}; last; } $ethsettings{"${uc}_STP"} = "on"; # network-hotplug-bridges expects "on" $ethsettings{"${uc}_STP_PRIORITY"} = $stp_priority; } } # validation failed, show error message and exit if ($VALIDATE_error) { &Header::openbox('100%', 'left', $Lang::tr{"error"}); print "$VALIDATE_error

$Lang::tr{'back'}\n"; &Header::closebox(); &Header::closebigbox(); &Header::closepage(); exit 0; } # new settings are valid, write configuration files &General::writehash("${General::swroot}/ethernet/settings",\%ethsettings); &General::writehash("${General::swroot}/ethernet/vlans",\%vlansettings); $restart_notice = $Lang::tr{'zoneconf notice reboot'}; } ### START OF TABLE ### &Header::openbox('100%', 'left', $Lang::tr{"zoneconf nic assignment"}); print < END ; # Fill the table header with all activated zones foreach (@zones) { my $uc = uc $_; # If the red zone is in PPP mode, don't show a mode dropdown if ($uc eq "RED") { my $red_type = $ethsettings{"RED_TYPE"}; unless (Network::is_red_mode_ip()) { print "\t\t\n"; next; # We're done here } } my %mode_selected = (); my $zone_mode = $ethsettings{"${uc}_MODE"}; if ($zone_mode eq "") { $mode_selected{"DEFAULT"} = "selected"; } elsif ($zone_mode eq "bridge") { $mode_selected{"BRIDGE"} = "selected"; } print <$uc
END ; } print "\t
\n"; # NIC assignment matrix foreach (@nics) { my $mac = $_->[0]; my $nic = $_->[1]; my $wlan = $_->[2]; print "\t\n"; print "\t\t\n"; # Iterate through all zones and check if the current NIC is assigned to it foreach (@zones) { my $uc = uc $_; my $highlight = ""; if ($uc eq "RED") { # VLANs/Bridging is not possible if the RED interface is set to PPP, PPPoE, VDSL, ... unless (Network::is_red_mode_ip()) { my $checked = ""; if ($mac eq $ethsettings{"${uc}_MACADDR"}) { $checked = "checked"; $highlight = $_; } print < END ; next; # We're done here } } my %access_selected = (); my $zone_mode = $ethsettings{"${uc}_MODE"}; my $zone_parent_dev = $vlansettings{"${uc}_PARENT_DEV"}; # ZONE_PARENT_DEV is set if this zone accesses any interface via a VLAN my $field_disabled = "disabled"; # Only enable the VLAN ID input field if the current access mode is VLAN my $zone_vlan_id = ""; # If ZONE_PARENT_DEV is set to a NICs name (e.g. green0 or eth0) instead of a MAC address, we have to find out this NICs MAC address $zone_parent_dev = &Network::get_mac_by_name($zone_parent_dev); # If the current NIC is accessed by the current zone via a VLAN, the ZONE_PARENT_DEV option corresponds to the current NIC if ($mac eq $zone_parent_dev) { $access_selected{"VLAN"} = "selected"; $field_disabled = ""; $zone_vlan_id = $vlansettings{"${uc}_VLAN_ID"}; } elsif ($zone_mode eq "bridge") { # If the current zone is in bridge mode, all corresponding NICs (Native as well as VLAN) are set via the ZONE_SLAVES option my @slaves = split(/ /, $ethsettings{"${uc}_SLAVES"}); foreach (@slaves) { # Slaves can be set to a NICs name so we have to find out its MAC address $_ = &Network::get_mac_by_name($_); if ($_ eq $mac) { $access_selected{"NATIVE"} = "selected"; last; } } } elsif ($mac eq $ethsettings{"${uc}_MACADDR"}) { # Native access via ZONE_MACADDR is only set if the zone does not access a NIC via a VLAN and the zone is not in bridge mode $access_selected{"NATIVE"} = "selected"; } $access_selected{"NONE"} = ($access_selected{"NATIVE"} eq "") && ($access_selected{"VLAN"} eq "") ? "selected" : ""; my $vlan_disabled = ($wlan) ? "disabled" : ""; # If the interface is assigned, hightlight table cell if ($access_selected{"NONE"} eq "") { $highlight = $_; } print < END ; } print "\t\n"; } # STP options my @stp_html = (); # form fields buffer (two rows) foreach (@zones) { # load settings and prepare form elements for each zone my $uc = uc $_; # STP is not available if the RED interface is set to PPP, PPPoE, VDSL, ... if ($uc eq "RED") { unless (Network::is_red_mode_ip()) { push(@stp_html, ["\t\t\n", "\t\t\n"]); # print empty cell next; } } # load configuration my $stp_available = $ethsettings{"${uc}_MODE"} eq "bridge"; # STP is only available in bridge mode my $stp_enabled = $ethsettings{"${uc}_STP"} eq "on"; my $stp_priority = $ethsettings{"${uc}_STP_PRIORITY"}; # set priority to default value if no numerical value is configured $stp_priority = 32768 unless looks_like_number($stp_priority); # form element modifiers my $checked = ""; my $disabled = ""; $checked = "checked" if ($stp_available && $stp_enabled); $disabled = "disabled" unless $stp_available; # enable checkbox HTML my $row_1 = < END ; $disabled = "disabled" unless $stp_enabled; # STP priority can't be entered if STP is disabled # priority input box HTML my $row_2 = < END ; # add fields to buffer push(@stp_html, [$row_1, $row_2]); } # print two rows of prepared form elements print < END ; foreach (@stp_html) { print $_->[0]; # row 1 } print < END ; foreach (@stp_html) { print $_->[1]; # row 2 } print "\t\n"; # footer and submit button print <
$restart_notice
END ; ### END OF TABLE ### &Header::closebox(); &Header::closebigbox(); &Header::closepage();
$uc ($red_type)
$nic
$mac
$Lang::tr{"zoneconf stp enable"}
$Lang::tr{"zoneconf stp priority"}