Files
Zakk d56c89ef4d fix(core): kill an orphaned core that still holds the ports, name the right binary
When the OOM killer takes v2rayA the core survives, is adopted by init and
keeps 20170/20171, so every later start failed with "port is already in use"
until someone killed it by hand (second half of v2rayA/v2rayA discussion
#1932). A process holding one of our ports that runs our core binary and whose
parent is init is ours to clean up.

The version mismatch also said only which two numbers differ, so users who had
installed xray-core concluded v2rayA was broken (#1930, #1903, #1891); it now
says which binary v2rayA needs.
2026-09-18 05:57:08 +10:00

164 lines
4.5 KiB
Go

package v2ray
import (
"errors"
"fmt"
"net"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/v2rayA/v2rayA/common"
"github.com/v2rayA/v2rayA/common/netTools/netstat"
"github.com/v2rayA/v2rayA/common/netTools/ports"
"github.com/v2rayA/v2rayA/db/configure"
"github.com/v2rayA/v2rayA/kernel/v2ray/where"
"github.com/v2rayA/v2rayA/pkg/util/log"
)
func (t *Template) checkDuplicatedTags() error {
inboundTagsSet := make(map[string]interface{})
for _, in := range t.Inbounds {
tag := in.Tag
if _, exists := inboundTagsSet[tag]; exists {
return fmt.Errorf("duplicated inbound tag: %v", tag)
} else {
inboundTagsSet[tag] = nil
}
}
outboundTagsSet := make(map[string]interface{})
for _, out := range t.Outbounds {
tag := out.Tag
if _, exists := outboundTagsSet[tag]; exists {
return fmt.Errorf("duplicated outbound tag: %v", tag)
} else {
outboundTagsSet[tag] = nil
}
}
return nil
}
func (t *Template) checkDuplicatedInboundSockets() error {
inboundSocketSet := make(map[string]interface{})
for _, in := range t.Inbounds {
if in.Listen == "" {
// https://www.v2fly.org/config/inbounds.html#inboundobject
in.Listen = "0.0.0.0"
}
socket := net.JoinHostPort(in.Listen, strconv.Itoa(in.Port))
if _, exists := inboundSocketSet[socket]; exists {
return fmt.Errorf("duplicated inbound listening address: %v", socket)
} else {
inboundSocketSet[socket] = nil
}
}
return nil
}
// killOrphanCore terminates a core process that was reparented to init after
// the v2rayA that started it died. It reports whether the port is free again.
func killOrphanCore(p *netstat.Process) bool {
if runtime.GOOS == "windows" || p.PPID != "1" {
return false
}
binPath, err := where.GetV2rayBinPath()
if err != nil || filepath.Base(binPath) != p.Name {
return false
}
pid, err := strconv.Atoi(p.PID)
if err != nil {
return false
}
proc, err := os.FindProcess(pid)
if err != nil {
return false
}
log.Warn("killing the orphaned %v (pid %v) that still holds the port", p.Name, p.PID)
if err := proc.Kill(); err != nil {
log.Warn("cannot kill the orphaned %v (pid %v): %v", p.Name, p.PID, err)
return false
}
// Not our child, so Wait cannot reap it; give the kernel a moment to drop
// the socket before the caller looks again.
_, _ = proc.Wait()
time.Sleep(200 * time.Millisecond)
return true
}
var OccupiedErr = fmt.Errorf("is already in use")
func PortOccupied(syntax []string) (err error) {
occupied, sockets, err := ports.IsPortOccupied(syntax)
if err != nil {
if errors.Is(err, netstat.ErrorNotSupportOSErr) {
log.Trace("PortOccupied: %v", err)
return nil
}
return
}
if occupied {
if err = netstat.FillProcesses(sockets); err != nil {
if errors.Is(err, netstat.ErrorNotSupportOSErr) {
log.Warn("cannot judge port occupation: %v", err)
return nil
}
return fmt.Errorf("failed to check if port is occupied: %w", err)
}
for _, s := range sockets {
p := s.Proc
if p == nil {
continue
}
if ownPID := strconv.Itoa(os.Getpid()); p.PPID == ownPID ||
p.PID == ownPID {
continue
}
// A core that outlived the v2rayA which started it — the OOM
// killer takes v2rayA first, init adopts the core and it keeps the
// ports, so every later start failed until someone killed it by
// hand. It is ours to clean up.
if killOrphanCore(p) {
continue
}
occupiedErr := fmt.Errorf("port %d %w by %v (pid %v)", s.LocalAddress.Port, OccupiedErr, p.Name, p.PID)
codedErr := common.Coded("PORT_OCCUPIED", occupiedErr, map[string]interface{}{"port": s.LocalAddress.Port})
if configure.GetSettingNotNil().PortSharing {
// want to listen 0.0.0.0, which conflicts with all IPs
return codedErr
}
if s.LocalAddress.IP.IsUnspecified() {
return codedErr
}
if s.LocalAddress.IP.IsLoopback() {
return codedErr
}
}
}
return nil
}
func (t *Template) CheckInboundPortsOccupied() (err error) {
var st []string
for _, in := range t.Inbounds {
switch strings.ToLower(in.Protocol) {
case "http", "vmess", "vless", "trojan":
st = append(st, strconv.Itoa(in.Port)+":tcp")
case "dokodemo-door":
if strings.HasPrefix(in.Tag, "dns-in") {
// checked before
continue
} else if in.Settings != nil && in.Settings.Network != "" {
st = append(st, strconv.Itoa(in.Port)+":"+in.Settings.Network)
} else {
st = append(st, strconv.Itoa(in.Port)+":tcp,udp")
}
default:
st = append(st, strconv.Itoa(in.Port)+":tcp,udp")
}
}
return PortOccupied(st)
}