Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ linters:
ignored-numbers:
- "2"

# テストのケース名・期待値はモード名と綴りが重なりがちだが定数化は不自然なので、
# goconst はテストファイルを数えない (exclusions の test 除外と方針を合わせる)。
goconst:
ignore-tests: true

staticcheck:
checks:
- all
Expand Down
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,16 @@ kame6 2001:200:dff:fff1:216:3eff:feb1:44d7
| netns | `name ADDR relay=NETNSNAME via=netns`(Linux・root) |
| vrf | `name ADDR relay=VRFNAME via=vrf`(Linux・root) |
| routeros | `name ADDR relay=ROS via=routeros_api username=U password=P method=https verify=false` |
| quic | `name ADDR via=quic [port=443] [sni=NAME] [verify=on]`(OS 非依存・外部コマンド不要) |
| tcp/hping3 | `name ADDR tcp=dstport:80`(Linux・root) |
| nexthop 強制 | `name ADDR nexthop=GWIP [source=eth0]`(直接 ICMP・Linux・root・IPv4/IPv6) |

ssh 中継の例 `google-via-ssh 173.194.117.176 relay=X.X.X.X os=Linux` は、リモートサーバ X.X.X.X 経由で対象へ ping します。
`user=USER` / `key=KEYPATH` で ssh のユーザ名と鍵を指定でき、`os` を省略すると deadman を実行している OS 名が使われます。
`os=` は中継先(Unix 系)の OS を表し、リモートで使う `ping`/`ping6` の選択や送信元フラグ(`-I`/`-S`)に影響します。Windows の中継先(`ping -n`)は未対応です。

`quic` モード(`via=quic`)は対象に QUIC(TLS1.3)ハンドシェイクを毎回張り、その**確立完了までの時間**を RTT として測ります(既定ポート 443・ALPN `h3`)。ICMP のような「任意 IP の死活」ではなく、QUIC/h3 を話すエンドポイント(ポート)の到達性を測る点で `tcp=` 接続プローブの仲間です。RTT は両端の TLS1.3 暗号処理を含むため ICMP よりやや高めに出ます。`port=` / `alpn=` / `sni=` / `verify=` で挙動を調整できます(「[属性](#属性)」参照)。外部コマンド不要・OS 非依存で、raw ソケット権限も要りません。

各モードの記述例は `deadman.conf` にもコメントとして含まれています。
中継モードが必要とする外部コマンドや権限は「[権限とプラットフォームに関する注意](#権限とプラットフォームに関する注意)」を参照してください。

Expand All @@ -98,11 +102,17 @@ ssh 中継の例 `google-via-ssh 173.194.117.176 relay=X.X.X.X os=Linux` は、
値は送信元 IP アドレス、または `source=eth0` のようなネットワークインターフェース名です。
インターフェース名を指定できるのは直接 ICMP・`nexthop`(Linux)と **Linux 上**の ssh/netns/vrf 中継に限ります。
macOS/BSD の中継先では `ping -S` が送信元**アドレス**のみを受け付けるため、ソースには IP アドレスを指定してください(インターフェース名は構築時に拒否され、起動時に警告が表示されます)。
snmp / routeros / tcp(hping3) モードは `source` を使いません(プローブは中継先で生成されるか hping3 が未対応のため)。指定しても無視され、起動時に警告が表示されます(対象の監視は継続します)。
snmp / routeros / tcp(hping3) / quic モードは `source` を使いません(プローブは中継先で生成されるか、hping3 が未対応か、QUIC のダイヤルが `[::]:0` を bind するため)。指定しても無視され、起動時に警告が表示されます(対象の監視は継続します)。

- **`verify=on|off`**(routeros / quic モード)… TLS 証明書検証の有無を指定します。
綴りは両モード共通で、`on` / `true` / `yes` / `1` が検証あり、`off` / `false` / `no` / `0` が検証なし(大文字小文字を問わない)。
ただし**既定値はモードで逆**です。
- routeros … 既定**有効**(安全側)。`off` 等でのみ無効化でき(自己署名証明書のネットワーク機器向け)、認識できない値は検証有効として扱います。
- quic … 既定**無効**(`InsecureSkipVerify`)。QUIC プローブは IP アドレスを直接対象にすることが多く、証明書/SAN 検査ではほぼ毎回失敗するためです。`on` 等で検証を有効化できます(認識できない値は無効のまま)。

- **`verify=on|off`**(routeros モード)… RouterOS REST API の TLS 証明書検証の有無を指定します
既定は**有効**で、`off` / `false` / `no` / `0`(大文字小文字を問わない)でのみ無効化できます(自己署名証明書のネットワーク機器向け)
`on` / `true` / `yes` / `1` や認識できない値は安全側(検証有効)として扱います
- **`port=` / `alpn=` / `sni=`**(quic モード)… QUIC プローブのダイヤル先ポート(既定 `443`)、ALPN(既定 `h3`)、TLS の SNI(`ServerName`)を指定します
`sni` を省略するとホスト名のアドレスがそのまま使われ、IP アドレスリテラルの場合は空になります
`InsecureSkipVerify`(検証無効)でも SNI は送出されるため、実在の h3 フロント(Cloudflare / Google など)を IP アドレスで対象にするときは `sni=` の指定が必要になることがあります

- **`resolve_family=ipv4|ipv6`** … ホスト名の解決をそのアドレスファミリーに固定します(`ipv4` は A レコード、`ipv6` は AAAA レコード)。
dual-stack なホスト名を IPv4 と IPv6 で別々に監視したいときに使います。
Expand All @@ -112,11 +122,11 @@ ssh 中継の例 `google-via-ssh 173.194.117.176 relay=X.X.X.X os=Linux` は、
web-v6 example.com resolve_family=ipv6
```

- **直接 ICMP に限り**有効で、`relay`/`via`/`tcp`/`nexthop` を併用した対象では無視されます(それらは中継先やゲートウェイ側でファミリーが決まるため)。
- **直接 ICMP と `via=quic` で**有効です(どちらも対象名をローカルで解決するため)。その他のモード(`relay`/`via=snmp/netns/vrf/routeros_api`/`tcp(hping3)`/`nexthop`を併用した対象では無視されます(中継先・ゲートウェイ側でファミリーが決まるか、そのモード自身のリゾルバ任せのため)。
- 値は `ipv4`/`ipv6` のみを認識し、それ以外(綴り違いなど)は指定なし(自動)として扱います。
- 指定したファミリーに該当レコードが無い場合や、反対のファミリーの IP アドレスリテラルを指定した場合は、その対象は `X`(到達不能)として表示されます
- 指定したファミリーに該当レコードが無いホスト名は、その対象が `X`(到達不能)になります。直接 ICMP では反対のファミリーの IP アドレスリテラルを指定した場合も `X` になりますが、`via=quic` では IP リテラルはファミリーが確定済みのため `resolve_family` の対象外です(そのままダイヤルします)
- 名前・アドレスが同じでも `resolve_family` が異なれば別エントリとして扱われ、リロード時の履歴も分かれて保持されます。
VIA 列はどちらも `direct` になるため、上の例のように行名で区別すると見分けやすくなります。
VIA 列は同じ表示(`direct` どうし/`quic PORT` どうし)になるため、上の例のように行名で区別すると見分けやすくなります。
- ホスト名が同じファミリー内に複数のアドレス(複数の A / AAAA)を持つ場合、probe するのは **RFC 6724 で並べ替えた先頭の 1 アドレスだけ**です(アドレス選択は Go / OS のリゾルバに委譲し、複数アドレスを並行して試す Happy Eyeballs 的な動作は行いません。これは `resolve_family` を指定しない場合も同じです)。
ラウンドロビン DNS では解決のたびに先頭が入れ替わりうるため、特定の 1 台を継続的に監視したい場合は IP アドレスを直接指定してください。

Expand Down Expand Up @@ -198,7 +208,7 @@ columns ADDRESS=off MIN=off MAX=off VIA=on
指定できるキーは`HOSTNAME ADDRESS VIA LOSS RTT AVG MIN MAX JIT SNT FAIL` です。
`RESULT`(結果バー)は deadman の看板のため常に表示され、隠せません。

`VIA` 列は各対象の**取得方法**を表示します(`direct` / `nexthop GWIP` / `ssh HOST` / `snmp HOST` / `netns NAME` / `vrf NAME` / `routeros HOST` / `tcp dstport:PORT`)。
`VIA` 列は各対象の**取得方法**を表示します(`direct` / `nexthop GWIP` / `ssh HOST` / `snmp HOST` / `netns NAME` / `vrf NAME` / `routeros HOST` / `tcp dstport:PORT` / `quic PORT`)。
同じアドレスを別経路で監視している場合などに、一目で区別できます。
表示は設定上の意図ではなく**実際に使われる経路**を反映するため、relay/via/tcp が優先されて nexthop が無視される対象では、その実際のモード(`ssh` など)が表示されます。

Expand Down Expand Up @@ -242,6 +252,7 @@ precision ms.1 統計値の表示精度(ms / ms.1 / ms.2 / ms.3 のいず
`ssh`(ssh 中継)、`snmpping`(snmp)、`ip`(netns/vrf)、`hping3`(tcp)が該当します。
netns・vrf・hping3 は Linux + root が前提です。
RouterOS API モードは HTTP を使うためOS 非依存です。
`quic` モード(`via=quic`)も in-process(quic-go による UDP ダイヤル)で動作し、外部コマンド不要・OS 非依存で、raw ソケット権限(root / `CAP_NET_RAW`)も要りません。
必要なコマンドが存在しない環境(たとえば Windows)では、その対象はクラッシュせず失敗(`X`)として表示されます。

`nexthop` 強制は AF_PACKET で L2 フレームを送り、ゲートウェイの MAC を IPv4 は ARP(`/proc/net/arp`)、IPv6 は NDP(netlink)で解決するため Linux + root/CAP_NET_RAW が必須で、他 OS のビルドではその対象は失敗(`X`)として表示されます。
Expand Down
6 changes: 6 additions & 0 deletions deadman.conf
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ kame6 2001:2f0:0:8800::1:1
# tcp/hping3 (Linux, root; may also work on macOS) -- TCP probe to a port.
#wide-tcp80 203.178.136.59 tcp=dstport:80

# quic (OS-independent, no external command) -- QUIC (TLS1.3) handshake RTT to a QUIC/h3
# endpoint. Cert verification defaults OFF; "port" defaults 443, "alpn" h3. "sni" sets the
# TLS ServerName -- a bare IP target against a real h3 front-end usually needs it.
#cloudflare-quic 1.1.1.1 via=quic sni=cloudflare-dns.com
#google-quic dns.google via=quic

# nexthop=GWIP -- force a direct ICMP probe out a next-hop gateway (Linux, root/CAP_NET_RAW).
# IPv4 (ARP) and IPv6 (NDP) both work; gateway and target must share the address family.
# The gateway must be on-link; name the egress with source=IFNAME or source=SRCIP (IFNAME is
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ require (
github.com/charmbracelet/x/ansi v0.11.7
github.com/mattn/go-runewidth v0.0.24
github.com/prometheus-community/pro-bing v0.9.0
golang.org/x/net v0.55.0
github.com/quic-go/quic-go v0.60.0
golang.org/x/net v0.56.0
golang.org/x/sys v0.46.0
)

Expand All @@ -29,6 +30,7 @@ require (
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/text v0.38.0 // indirect
)
20 changes: 18 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
Expand All @@ -34,20 +36,34 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus-community/pro-bing v0.9.0 h1:M/zt1lL7cmbK+wm40RPS4waaiAN2EcYEeD4Xt0Lv4x0=
github.com/prometheus-community/pro-bing v0.9.0/go.mod h1:IBeW2ScY7sAWv4mYjH0xDDigqfe7kXeVxNdbNKdBHWY=
github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0=
github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
3 changes: 3 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,14 @@ var reSeparator = regexp.MustCompile(`^-+$`)
// deliberately absent: the netns mode is selected by via=netns and reads the namespace
// name from relay=, so a `netns=` token is a typo that should surface as an ignored
// stray token (the warning every other unknown key gets), not be silently swallowed.
// "port"/"alpn"/"sni" are QUIC (via=quic) sub-attributes; like "method"/"verify" they
// are meaningful only to their mode and harmlessly ignored by the others.
var relayKeys = map[string]bool{
"os": true, "relay": true, "via": true, "community": true,
"user": true, "key": true, "method": true,
"username": true, "password": true, "verify": true,
"nexthop": true, "resolve_family": true,
"port": true, "alpn": true, "sni": true,
}

// ParseConfig reads a deadman config from r and returns the parsed targets plus
Expand Down
28 changes: 28 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,34 @@ func TestParseConfigResolveFamily(t *testing.T) {
}
}

func TestParseConfigQUIC(t *testing.T) {
// The QUIC sub-attributes (port/alpn/sni) are relay keys, so they land in the
// relay map verbatim alongside via/verify; none is recorded in Dropped as unknown.
cfg, err := ParseConfig(
strings.NewReader("h 1.2.3.4 via=quic port=8443 alpn=h3 sni=x verify=on\n"),
)
if err != nil {
t.Fatal(err)
}

specs := cfg.Targets
if len(specs) != 1 {
t.Fatalf("got %d specs, want 1: %+v", len(specs), specs)
}

for k, want := range map[string]string{
"via": "quic", "port": "8443", "alpn": "h3", "sni": "x", "verify": "on",
} {
if specs[0].Relay[k] != want {
t.Errorf("Relay[%q] = %q, want %q", k, specs[0].Relay[k], want)
}
}

if len(specs[0].Dropped) != 0 {
t.Errorf("Dropped = %v, want empty (quic attrs must not be dropped)", specs[0].Dropped)
}
}

func TestParseConfigDroppedTokens(t *testing.T) {
// A name with spaces shifts real tokens past the address slot: this parses to
// name="Cloudflare", address="via", with "MGMT" and "1.1.1.1" unroutable and
Expand Down
Loading
Loading